Skip to content

Commit 7de3591

Browse files
yoneymeta-codesync[bot]
authored andcommitted
Add free-threaded JIT dict lookup tests
Summary: Add coverage for JIT dict lookups during concurrent updates and with dict subclasses. Reviewed By: DinoV Differential Revision: D116945515 fbshipit-source-id: 66d0c2ed1d0022c8179a954f4ee64005418fa49b
1 parent 4288569 commit 7de3591

1 file changed

Lines changed: 174 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
# pyre-strict
4+
5+
"""Free-threaded JIT regression tests for dict subscripts."""
6+
7+
import dis
8+
import threading
9+
import unittest
10+
from collections.abc import Callable
11+
from concurrent.futures import ThreadPoolExecutor
12+
13+
import cinderx.jit
14+
from cinderx.test_support import run_in_subprocess
15+
16+
17+
class JITDictTest(unittest.TestCase):
18+
def warm_up_dict_opcode(
19+
self,
20+
read_item: Callable[[dict[str, str]], str],
21+
) -> None:
22+
values = {"key": "w"}
23+
cinderx.jit.jit_suppress(read_item)
24+
for _ in range(100):
25+
read_item(values)
26+
cinderx.jit.jit_unsuppress(read_item)
27+
opnames = {
28+
instruction.opname
29+
for instruction in dis.get_instructions(read_item, adaptive=True)
30+
}
31+
self.assertIn("BINARY_OP_SUBSCR_DICT", opnames)
32+
33+
def exercise_concurrent_access(
34+
self,
35+
read_item: Callable[[dict[str, str]], str],
36+
) -> None:
37+
worker_count = 10
38+
reader_count = worker_count // 2
39+
writer_count = worker_count - reader_count
40+
iterations = 10_000
41+
start = threading.Barrier(worker_count)
42+
values = {"key": "w"}
43+
44+
@cinderx.jit.jit_suppress
45+
def reader() -> bool:
46+
start.wait()
47+
for _ in range(iterations):
48+
if not read_item(values).startswith("w"):
49+
return False
50+
return True
51+
52+
@cinderx.jit.jit_suppress
53+
def writer(prefix: str) -> None:
54+
start.wait()
55+
for i in range(iterations):
56+
values["key"] = f"w{prefix}_{i}"
57+
# Unique transient keys force periodic key-table replacement.
58+
transient_key = f"transient_{prefix}_{i}"
59+
values[transient_key] = "w"
60+
del values[transient_key]
61+
62+
with ThreadPoolExecutor(max_workers=worker_count) as executor:
63+
reader_futures = [executor.submit(reader) for _ in range(reader_count)]
64+
writer_futures = [
65+
executor.submit(writer, str(worker)) for worker in range(writer_count)
66+
]
67+
68+
self.assertEqual(
69+
[future.result() for future in reader_futures],
70+
[True] * reader_count,
71+
)
72+
for future in writer_futures:
73+
future.result()
74+
75+
self.assertTrue(values["key"].startswith("w"))
76+
77+
def exercise_concurrent_reads(
78+
self,
79+
read_item: Callable[[dict[str, str]], str],
80+
values: dict[str, str],
81+
expected: str,
82+
) -> int:
83+
worker_count = 10
84+
iterations = 10_000
85+
start = threading.Barrier(worker_count)
86+
87+
@cinderx.jit.jit_suppress
88+
def reader(_: int) -> bool:
89+
start.wait()
90+
for _ in range(iterations):
91+
if read_item(values) != expected:
92+
return False
93+
return True
94+
95+
with ThreadPoolExecutor(max_workers=worker_count) as executor:
96+
results = list(executor.map(reader, range(worker_count)))
97+
98+
self.assertEqual(results, [True] * worker_count)
99+
return worker_count * iterations
100+
101+
@run_in_subprocess
102+
def test_concurrent_subscript_without_specialized_opcodes(self) -> None:
103+
"""Keep generic HIR when compiling an adaptive dict opcode."""
104+
cinderx.jit.disable_specialized_opcodes()
105+
106+
def read_item(values: dict[str, str]) -> str:
107+
return values["key"]
108+
109+
self.warm_up_dict_opcode(read_item)
110+
111+
self.assertTrue(cinderx.jit.force_compile(read_item))
112+
opcode_counts = cinderx.jit.get_function_hir_opcode_counts(read_item)
113+
if opcode_counts is None:
114+
self.fail("No HIR opcode counts for compiled read_item")
115+
self.assertIn("BinaryOp", opcode_counts)
116+
self.assertNotIn("DictSubscr", opcode_counts)
117+
118+
self.exercise_concurrent_access(read_item)
119+
120+
@run_in_subprocess
121+
def test_concurrent_subscript_with_simplify(self) -> None:
122+
"""Stress DictSubscr during concurrent value and key-table mutation."""
123+
cinderx.jit.enable_specialized_opcodes()
124+
125+
def read_item(values: dict[str, str]) -> str:
126+
return values["key"]
127+
128+
self.warm_up_dict_opcode(read_item)
129+
130+
self.assertTrue(cinderx.jit.force_compile(read_item))
131+
opcode_counts = cinderx.jit.get_function_hir_opcode_counts(read_item)
132+
if opcode_counts is None:
133+
self.fail("No HIR opcode counts for compiled read_item")
134+
self.assertIn("DictSubscr", opcode_counts)
135+
self.assertNotIn("BinaryOp", opcode_counts)
136+
137+
self.exercise_concurrent_access(read_item)
138+
139+
@run_in_subprocess
140+
def test_concurrent_deopt_on_guard_failure(self) -> None:
141+
"""Concurrent dict-subclass calls deopt at the exact-dict guard."""
142+
cinderx.jit.enable_specialized_opcodes()
143+
144+
def read_item(values: dict[str, str]) -> str:
145+
return values["key"]
146+
147+
self.warm_up_dict_opcode(read_item)
148+
self.assertTrue(cinderx.jit.force_compile(read_item))
149+
150+
class OverrideDict(dict[str, str]):
151+
def __getitem__(self, key: str) -> str:
152+
return "override"
153+
154+
cinderx.jit.get_and_clear_runtime_stats()
155+
call_count = self.exercise_concurrent_reads(
156+
read_item,
157+
OverrideDict(key="stored"),
158+
"override",
159+
)
160+
deopts = cinderx.jit.get_and_clear_runtime_stats()["deopt"]
161+
if not isinstance(deopts, list):
162+
self.fail("Deopt runtime stats are not a list")
163+
read_item_deopts = [
164+
deopt
165+
for deopt in deopts
166+
if deopt["normal"]["func_qualname"] == read_item.__qualname__
167+
]
168+
self.assertTrue(read_item_deopts)
169+
for deopt in read_item_deopts:
170+
self.assertEqual(deopt["normal"]["reason"], "GuardFailure")
171+
self.assertEqual(deopt["normal"]["description"], "GuardType")
172+
self.assertEqual(
173+
sum(deopt["int"]["count"] for deopt in read_item_deopts), call_count
174+
)

0 commit comments

Comments
 (0)