Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/linux-sdk-contracts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
- name: Header scanner regressions and public dependency closure
run: |
python3 eng/sdk/test-header-closure.py
python3 eng/sdk/test-verify-parser-components.py
python3 eng/sdk/test-qualify-macos-profile.py
python3 - <<'PY'
import importlib.util
Expand Down
24 changes: 24 additions & 0 deletions docs/validation/parser-component-size-budget-671-229.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Parser component size budget (#671 / #229)

The CSS/selector parser archive is a reusable SDK component. It must remain
free of the runtime HTML parser while CSS and selector coverage grows.

`eng/sdk/verify-parser-components.py` enforces three independent boundaries:

- required CSS and selector ABI symbols must be present;
- HTML ABI symbols and `html5ever::` symbols must be absent;
- the focused archive must be no more than 90% of the full parser archive and
no more than 18,000,000 bytes.

The ratio detects a collapsed component split. The absolute limit detects
package growth even when the full parser grows at the same rate. Symbol checks
remain the semantic proof that HTML code was not retained.

The Linux observation that triggered this update was 16,511,566 bytes for the
CSS/selector archive and 19,346,060 bytes for the full archive, a ratio of
0.853485. Both byte values and both configured limits are emitted in
`parser-components.json` for later release/package-size comparison.

`eng/sdk/test-verify-parser-components.py` locks the passing boundaries and
independent failures for ratio, bytes, missing CSS ABI, retained HTML ABI, and
retained `html5ever` symbols.
54 changes: 54 additions & 0 deletions eng/sdk/test-verify-parser-components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import importlib.util
from pathlib import Path
import unittest


SCRIPT = Path(__file__).with_name("verify-parser-components.py")
SPEC = importlib.util.spec_from_file_location("verify_parser_components", SCRIPT)
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)


class VerifyParserComponentsTests(unittest.TestCase):
@staticmethod
def full_symbols():
return "\n".join(MODULE.HTML_SYMBOLS + MODULE.CSS_SYMBOLS)

@staticmethod
def css_symbols():
return "\n".join(MODULE.CSS_SYMBOLS)

def test_accepts_both_budget_boundaries(self):
report = MODULE.verify_components(
self.full_symbols(), self.css_symbols(), 20_000_000, 18_000_000)
self.assertEqual(report["maximumCssToFullSizeRatio"], 0.90)
self.assertEqual(report["maximumCssSelectorArchiveBytes"], 18_000_000)

def test_ratio_budget_fails_independently(self):
with self.assertRaisesRegex(RuntimeError, "size ratio"):
MODULE.verify_components(
self.full_symbols(), self.css_symbols(), 10_000_000, 9_000_001)

def test_absolute_budget_fails_independently(self):
with self.assertRaisesRegex(RuntimeError, "archive size"):
MODULE.verify_components(
self.full_symbols(), self.css_symbols(), 25_000_000, 18_000_001)

def test_missing_css_abi_fails(self):
with self.assertRaisesRegex(RuntimeError, "missingCss"):
MODULE.verify_components(
self.full_symbols(), self.css_symbols().replace(MODULE.CSS_SYMBOLS[0], ""),
20_000_000, 10_000_000)

def test_retained_html_symbols_fail(self):
for retained in (MODULE.HTML_SYMBOLS[0], "html5ever::"):
with self.subTest(retained=retained), self.assertRaisesRegex(
RuntimeError, "retainedHtml"):
MODULE.verify_components(
self.full_symbols(), self.css_symbols() + "\n" + retained,
20_000_000, 10_000_000)


if __name__ == "__main__":
unittest.main()
49 changes: 30 additions & 19 deletions eng/sdk/verify-parser-components.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,16 @@
"webscene_selector_parser_abi_version",
"webscene_selector_parse",
)
MAXIMUM_CSS_TO_FULL_SIZE_RATIO = 0.90
MAXIMUM_CSS_SELECTOR_ARCHIVE_BYTES = 18_000_000


def symbols(nm, archive):
return subprocess.check_output(
[str(nm), "-g", "-C", str(archive)], text=True, stderr=subprocess.STDOUT)


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--nm", required=True, type=Path)
parser.add_argument("--full", required=True, type=Path)
parser.add_argument("--css", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
for path in (args.nm, args.full, args.css):
if not path.is_file():
parser.error(f"missing input: {path}")
full = symbols(args.nm, args.full)
css = symbols(args.nm, args.css)
def verify_components(full, css, full_size, css_size):
missing_full = [name for name in HTML_SYMBOLS + CSS_SYMBOLS if name not in full]
missing_css = [name for name in CSS_SYMBOLS if name not in css]
retained_html = [name for name in HTML_SYMBOLS if name in css]
Expand All @@ -47,20 +38,40 @@ def main():
"missingCss": missing_css,
"retainedHtml": retained_html,
}, sort_keys=True))
full_size = args.full.stat().st_size
css_size = args.css.stat().st_size
ratio = css_size / full_size
if ratio > 0.85:
if ratio > MAXIMUM_CSS_TO_FULL_SIZE_RATIO:
raise RuntimeError(
f"CSS/selector archive size ratio {ratio:.6f} exceeds 0.85 "
f"({css_size}/{full_size})")
report = {
f"CSS/selector archive size ratio {ratio:.6f} exceeds "
f"{MAXIMUM_CSS_TO_FULL_SIZE_RATIO:.2f} ({css_size}/{full_size})")
if css_size > MAXIMUM_CSS_SELECTOR_ARCHIVE_BYTES:
raise RuntimeError(
f"CSS/selector archive size {css_size} exceeds "
f"{MAXIMUM_CSS_SELECTOR_ARCHIVE_BYTES} bytes")
return {
"schemaVersion": 1,
"fullArchive": {"bytes": full_size, "html": True, "css": True, "selectors": True},
"cssSelectorArchive": {"bytes": css_size, "html": False, "css": True, "selectors": True},
"cssToFullSizeRatio": ratio,
"maximumCssToFullSizeRatio": 0.85,
"maximumCssToFullSizeRatio": MAXIMUM_CSS_TO_FULL_SIZE_RATIO,
"maximumCssSelectorArchiveBytes": MAXIMUM_CSS_SELECTOR_ARCHIVE_BYTES,
}


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--nm", required=True, type=Path)
parser.add_argument("--full", required=True, type=Path)
parser.add_argument("--css", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
for path in (args.nm, args.full, args.css):
if not path.is_file():
parser.error(f"missing input: {path}")
full = symbols(args.nm, args.full)
css = symbols(args.nm, args.css)
full_size = args.full.stat().st_size
css_size = args.css.stat().st_size
report = verify_components(full, css, full_size, css_size)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps(report, sort_keys=True))
Expand Down
Loading