Skip to content

Commit ea1dd41

Browse files
committed
gh-161106: Use repeat counts in plistlib binary writer struct formats
Compiled struct formats with one code per item stay in the struct module's cache and take about 33 bytes per item. Use the repeat-count form the reader already uses; the output is unchanged.
1 parent 9c0cfb1 commit ea1dd41

3 files changed

Lines changed: 27 additions & 4 deletions

File tree

‎Lib/plistlib.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ def write(self, value):
699699
top_object = self._getrefnum(value)
700700
offset_table_offset = self._fp.tell()
701701
offset_size = _count_to_size(offset_table_offset)
702-
offset_format = '>' + _BINARY_FORMAT[offset_size] * num_objects
702+
offset_format = f'>{num_objects}{_BINARY_FORMAT[offset_size]}'
703703
self._fp.write(struct.pack(offset_format, *self._object_offsets))
704704

705705
# Write trailer
@@ -846,7 +846,7 @@ def _write_object(self, value):
846846
refs = [self._getrefnum(o) for o in value]
847847
s = len(refs)
848848
self._write_size(0xA0, s)
849-
self._fp.write(struct.pack('>' + self._ref_format * s, *refs))
849+
self._fp.write(struct.pack(f'>{s}{self._ref_format}', *refs))
850850

851851
elif isinstance(value, (dict, frozendict)):
852852
keyRefs, valRefs = [], []
@@ -862,8 +862,8 @@ def _write_object(self, value):
862862

863863
s = len(keyRefs)
864864
self._write_size(0xD0, s)
865-
self._fp.write(struct.pack('>' + self._ref_format * s, *keyRefs))
866-
self._fp.write(struct.pack('>' + self._ref_format * s, *valRefs))
865+
self._fp.write(struct.pack(f'>{s}{self._ref_format}', *keyRefs))
866+
self._fp.write(struct.pack(f'>{s}{self._ref_format}', *valRefs))
867867

868868
else:
869869
raise TypeError(value)

‎Lib/test/test_plistlib.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import operator
44
import pickle
55
import struct
6+
import tracemalloc
67
import unittest
78
import plistlib
89
import os
@@ -1037,6 +1038,25 @@ def test_dump_duplicates(self):
10371038
data = plistlib.dumps([x]*1000, fmt=plistlib.FMT_BINARY)
10381039
self.assertLess(len(data), 1100, repr(data))
10391040

1041+
def test_dump_large_container_memory(self):
1042+
# Writing a large container should not leave memory proportional
1043+
# to its size allocated after the call returns.
1044+
if not tracemalloc.is_tracing():
1045+
tracemalloc.start()
1046+
self.addCleanup(tracemalloc.stop)
1047+
n = 20_000
1048+
for make in (lambda n: [True] * n,
1049+
lambda n: dict.fromkeys(map(str, range(n)), True)):
1050+
x = make(n)
1051+
with self.subTest(type=type(x).__name__):
1052+
# Warm up with a slightly smaller container, so that
1053+
# memory kept by free lists is not counted.
1054+
plistlib.dumps(make(n - 1), fmt=plistlib.FMT_BINARY)
1055+
before, _ = tracemalloc.get_traced_memory()
1056+
plistlib.dumps(x, fmt=plistlib.FMT_BINARY)
1057+
after, _ = tracemalloc.get_traced_memory()
1058+
self.assertLess(after - before, n)
1059+
10401060
def test_identity(self):
10411061
for x in (None, False, True, 12345, 123.45, 'abcde', b'abcde',
10421062
datetime.datetime(2004, 10, 26, 10, 33, 33),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :func:`plistlib.dump` and :func:`plistlib.dumps` with
2+
:data:`~plistlib.FMT_BINARY` holding on to memory proportional to the size of
3+
large arrays and dictionaries after the call returns.

0 commit comments

Comments
 (0)