From cc9cf09516715d0135ef0f7b19f4a46b937ffdfd Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Tue, 23 Dec 2025 09:39:46 -0500 Subject: [PATCH 1/3] gh-142145: Avoid timing measurements in quadratic behavior test Count the number of Element attribute accesses as a proxy for work done. With double the amount of work, a ratio of 2.0 indicates linear scaling and 4.0 quadratic scaling. Use 3.2 as an intermediate threshold. --- Lib/test/test_minidom.py | 50 +++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 69fae957ec7fc9..dbcd93fab6ee9d 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -174,27 +174,41 @@ def testAppendChild(self): self.assertEqual(dom.documentElement.childNodes[-1].data, "Hello") dom.unlink() - @support.requires_resource('cpu') def testAppendChildNoQuadraticComplexity(self): + # Don't use wall-clock timing (too flaky). Instead count a proxy for the + # old quadratic behavior: repeated attribute access, such as of + # parentNode/nodeType during document-membership checks. impl = getDOMImplementation() - newdoc = impl.createDocument(None, "some_tag", None) - top_element = newdoc.documentElement - children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)] - element = top_element - - start = time.monotonic() - for child in children: - element.appendChild(child) - element = child - end = time.monotonic() - - # This example used to take at least 30 seconds. - # Conservative assertion due to the wide variety of systems and - # build configs timing based tests wind up run under. - # A --with-address-sanitizer --with-pydebug build on a rpi5 still - # completes this loop in <0.5 seconds. - self.assertLess(end - start, 4) + def work(n): + doc = impl.createDocument(None, "some_tag", None) + element = doc.documentElement + total_calls = 0 + + def getattribute_counter(self, attr): + nonlocal total_calls + total_calls += 1 + return object.__getattribute__(self, attr) + + with support.swap_attr(Element, "__getattribute__", getattribute_counter): + for _ in range(n): + child = doc.createElement("child") + element.appendChild(child) + element = child + return total_calls + + # Doubling N should not ~quadruple the work. + w1 = work(1024) + w2 = work(2048) + w3 = work(4096) + + self.assertGreater(w1, 0) + r1 = w2 / w1 + r2 = w3 / w2 + self.assertLess( + max(r1, r2), 3.2, + msg=f"Possible quadratic behavior: work={w1,w2,w3} ratios={r1,r2}" + ) def testSetAttributeNodeWithoutOwnerDocument(self): # regression test for gh-142754 From 506e9781eca52d97a2ea17ac3353f247c92a06dd Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Tue, 23 Dec 2025 11:21:35 -0500 Subject: [PATCH 2/3] Lint --- Lib/test/test_minidom.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index dbcd93fab6ee9d..c74844a6f3d006 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -2,7 +2,6 @@ import copy import pickle -import time import io from test import support import unittest From ec362b35ad4ecf1858dbf0af8e9057d721c3a259 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Tue, 23 Dec 2025 14:48:18 -0500 Subject: [PATCH 3/3] Changes from review --- Lib/test/test_minidom.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index c74844a6f3d006..46249e5138aed5 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -173,10 +173,8 @@ def testAppendChild(self): self.assertEqual(dom.documentElement.childNodes[-1].data, "Hello") dom.unlink() + @support.requires_resource('cpu') def testAppendChildNoQuadraticComplexity(self): - # Don't use wall-clock timing (too flaky). Instead count a proxy for the - # old quadratic behavior: repeated attribute access, such as of - # parentNode/nodeType during document-membership checks. impl = getDOMImplementation() def work(n): @@ -184,6 +182,7 @@ def work(n): element = doc.documentElement total_calls = 0 + # Count attribute accesses as a proxy for work done def getattribute_counter(self, attr): nonlocal total_calls total_calls += 1