Skip to content

Commit d2876cd

Browse files
authored
Merge commit from fork
* Limit unpacker container preallocation to 1000 items * Increase unpacker preallocation limit to 10000 items
1 parent 3650629 commit d2876cd

3 files changed

Lines changed: 87 additions & 9 deletions

File tree

‎msgpack/fallback.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ def _unpack(self, execute=EX_CONSTRUCT):
518518
# TODO check whether we need to call `list_hook`
519519
self._unpack(EX_SKIP)
520520
return
521-
ret = newlist_hint(n)
521+
ret = newlist_hint(min(n, 10000))
522522
for i in range(n):
523523
ret.append(self._unpack(EX_CONSTRUCT))
524524
if self._list_hook is not None:

‎msgpack/unpack.h‎

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818

1919
#define MSGPACK_EMBED_STACK_SIZE (1024)
20+
#define MSGPACK_MAX_PREALLOC (10000)
2021
#include "unpack_define.h"
2122

2223
typedef struct unpack_user {
@@ -131,28 +132,48 @@ static inline int unpack_callback_array(unpack_user* u, unsigned int n, msgpack_
131132
PyErr_Format(PyExc_ValueError, "%u exceeds max_array_len(%zd)", n, u->max_array_len);
132133
return -1;
133134
}
134-
PyObject *p = u->use_list ? PyList_New(n) : PyTuple_New(n);
135+
/* Large tuples are built as lists and converted when complete. */
136+
PyObject *p = n > MSGPACK_MAX_PREALLOC ? PyList_New(MSGPACK_MAX_PREALLOC) :
137+
(u->use_list ? PyList_New(n) : PyTuple_New(n));
135138

136139
if (!p)
137140
return -1;
138141
*o = p;
139142
return 0;
140143
}
141144

145+
/* Consumes o on both success and failure; borrows the list. */
146+
static inline int unpack_list_item(PyObject *list, unsigned int current, PyObject *o)
147+
{
148+
if (current < MSGPACK_MAX_PREALLOC) {
149+
PyList_SET_ITEM(list, current, o);
150+
return 0;
151+
}
152+
int ret = PyList_Append(list, o);
153+
Py_DECREF(o);
154+
return ret;
155+
}
156+
142157
/* Consumes o on both success and failure; borrows the container. */
143158
static inline int unpack_callback_array_item(unpack_user* u, unsigned int current, msgpack_unpack_object* c, msgpack_unpack_object o)
144159
{
145-
if (u->use_list)
146-
PyList_SET_ITEM(*c, current, o);
147-
else
148-
PyTuple_SET_ITEM(*c, current, o);
160+
if (PyList_CheckExact(*c))
161+
return unpack_list_item(*c, current, o);
162+
PyTuple_SET_ITEM(*c, current, o);
149163
return 0;
150164
}
151165

152166
/* Replaces the owned container reference with the hook result, or NULL on
153167
* failure. The original reference is consumed in either case. */
154168
static inline int unpack_callback_array_end(unpack_user* u, msgpack_unpack_object* c)
155169
{
170+
if (!u->use_list && PyList_CheckExact(*c)) {
171+
PyObject *tuple = PyList_AsTuple(*c);
172+
Py_DECREF(*c);
173+
*c = tuple;
174+
if (!tuple)
175+
return -1;
176+
}
156177
if (u->list_hook) {
157178
PyObject *new_c = PyObject_CallFunctionObjArgs(u->list_hook, *c, NULL);
158179
Py_DECREF(*c);
@@ -171,7 +192,7 @@ static inline int unpack_callback_map(unpack_user* u, unsigned int n, msgpack_un
171192
}
172193
PyObject *p;
173194
if (u->has_pairs_hook) {
174-
p = PyList_New(n); // Or use tuple?
195+
p = PyList_New(n > MSGPACK_MAX_PREALLOC ? MSGPACK_MAX_PREALLOC : n);
175196
}
176197
else {
177198
p = PyDict_New();
@@ -200,8 +221,7 @@ static inline int unpack_callback_map_item(unpack_user* u, unsigned int current,
200221
Py_DECREF(v);
201222
if (!item)
202223
return -1;
203-
PyList_SET_ITEM(*c, current, item);
204-
return 0;
224+
return unpack_list_item(*c, current, item);
205225
}
206226
int ret = PyDict_SetItem(*c, k, v);
207227
Py_DECREF(k);

‎test/test_limits.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#!/usr/bin/env python
22
import sysconfig
3+
import tracemalloc
34

45
import pytest
56

67
from msgpack import (
78
ExtType,
9+
OutOfData,
810
Packer,
911
PackOverflowError,
1012
PackValueError,
@@ -118,6 +120,62 @@ def test_max_map_len():
118120
unpacker.unpack()
119121

120122

123+
@pytest.mark.parametrize("size", [9999, 10000, 10001, 20001])
124+
@pytest.mark.parametrize("use_list", [False, True])
125+
def test_array_preallocation_boundary(size, use_list):
126+
value = list(range(size))
127+
expected = value if use_list else tuple(value)
128+
packed = packb(value)
129+
assert unpackb(packed, use_list=use_list, list_hook=lambda obj: obj) == expected
130+
131+
unpacker = Unpacker(use_list=use_list)
132+
# Suspend after crossing the preallocation boundary.
133+
unpacker.feed(packed[:-1])
134+
with pytest.raises(OutOfData):
135+
unpacker.unpack()
136+
unpacker.feed(packed[-1:])
137+
assert unpacker.unpack() == expected
138+
139+
140+
@pytest.mark.parametrize("size", [9999, 10000, 10001, 20001])
141+
def test_pairs_preallocation_boundary(size):
142+
value = {str(i): i for i in range(size)}
143+
expected = list(value.items())
144+
packed = packb(value)
145+
assert unpackb(packed, object_pairs_hook=list) == expected
146+
147+
unpacker = Unpacker(object_pairs_hook=list)
148+
unpacker.feed(packed[:-1])
149+
with pytest.raises(OutOfData):
150+
unpacker.unpack()
151+
unpacker.feed(packed[-1:])
152+
assert unpacker.unpack() == expected
153+
154+
155+
@pytest.mark.parametrize("kind", ["list", "tuple", "pairs"])
156+
def test_nested_headers_preallocation(kind):
157+
packer = Packer()
158+
header = (
159+
packer.pack_map_header(100_000) if kind == "pairs" else packer.pack_array_header(100_000)
160+
)
161+
unpacker = Unpacker(
162+
use_list=kind != "tuple", object_pairs_hook=list if kind == "pairs" else None
163+
)
164+
# Map headers nest in key position, so no completed pairs are needed.
165+
unpacker.feed(header * 16)
166+
already_tracing = tracemalloc.is_tracing()
167+
tracemalloc.start()
168+
try:
169+
before = tracemalloc.get_traced_memory()[0]
170+
with pytest.raises(OutOfData):
171+
unpacker.unpack()
172+
# Full preallocation would retain over 12 MB in the C unpacker.
173+
assert tracemalloc.get_traced_memory()[0] - before < 4 * 1024 * 1024
174+
finally:
175+
if not already_tracing:
176+
tracemalloc.stop()
177+
178+
121179
def test_max_ext_len():
122180
d = ExtType(42, b"abc")
123181
packed = packb(d)

0 commit comments

Comments
 (0)