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
4 changes: 4 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ jobs:
run: |
python -m unittest scripts/test_css_capability_matrix.py -v
python scripts/css_capability_matrix.py --check
- name: CSS effective-property metadata contracts
run: |
python -m unittest scripts/test_css_property_metadata.py -v
python tools/generate_css_property_metadata.py --check
- name: Code OSS Web API ledger contracts
run: |
python -m unittest scripts/test_code_oss_web_api_ledger.py -v
Expand Down
72 changes: 72 additions & 0 deletions docs/validation/css-property-metadata-20260917.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Generated effective-property metadata validation (2026-09-17)

This stage of #235 moves the effective-property knowledge introduced by #305
out of cascade-specific conditionals and into a generated registry. The source
catalog is `experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json`;
`tools/generate_css_property_metadata.py` validates it and emits the sorted,
constexpr native table.

## Structural result

The registry currently contains 61 physical, logical, and shorthand names for
margin, padding, inset, border width/color, gap, and overflow. Each row owns its
modeled style mask and zero, one, two, or four effective longhands. Cascade
rollback and property-mask lookup now consume that shared data instead of
maintaining separate property-name lists.

Six logical shorthands whose prior ordinary application path lost their second
component are marked for application-time expansion: `inset-block`,
`inset-inline`, `border-block-width`, `border-inline-width`,
`border-block-color`, and `border-inline-color`. Other common shorthands retain
their existing direct application path; high-frequency direct properties such
as width, display, background, color, font, and transform still return before a
metadata lookup. Expansion remains allocation-free until one of those six
logical shorthands or the rare rollback path is actually used.

The generator rejects duplicate names, unknown effective targets, invalid mask
symbols, invalid expansion arity, and stale checked-in output. CI runs both its
unit tests and `--check` mode.

## Browser and native behavior

The browser-referenced effective-property contract was extended from 12 to 15
subtests. The three additions cover logical inset rollback, logical border
width/color rollback, and two-value inline CSSOM application followed by a
recascade.

- Chrome 153.0.8010.48: 15/15
- native current CSSParser: 15/15
- native legacy parser: 15/15
- unchanged cascade winner matrix: 17/17
- adjacent all/margin/CSSOM contracts: 21/21
- NativeWeb CSS service/shared styles/compiler: 3/3 CTest

The cascade-layer mutation gate retains exact work invariance when unrelated
nodes grow from 32 to 1,024:

| affected | unrelated | selector candidates | fallback visits | rule checks | applications | cascade candidates |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 8 | 32 | 16 | 0 | 32 | 18 | 32 |
| 8 | 1024 | 16 | 0 | 32 | 18 | 32 |
| 128 | 32 | 256 | 0 | 512 | 258 | 512 |
| 128 | 1024 | 256 | 0 | 512 | 258 | 512 |

## Lookup A/B

`webscene_css_property_metadata_benchmark` performs 24,000,000 lookups per run
over a fixed mix of 15 direct hot properties and nine effective-property
entries. For the retained A/B, the same benchmark source and AppleClang 21
`-O3` command were compiled once against merged #305 and once against this
branch. Nine alternating runs produced:

- merged #305 median: 502.446 ms (495.039–521.802 ms)
- generated registry median: 424.271 ms (418.068–440.586 ms)
- median change: -15.56%

This microbenchmark establishes that centralization did not trade maintainable
metadata for slower property dispatch. It is not an end-to-end Spotify resize
claim; physical resize and media-threshold acceptance remains later work in
#235.

Machine-readable evidence is in
`docs/validation/evidence/css-property-metadata-20260917.json`.
30 changes: 30 additions & 0 deletions docs/validation/evidence/css-property-metadata-20260917.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"date": "2026-09-17",
"issue": 235,
"catalogEntries": 61,
"applicationExpandedShorthands": 6,
"contracts": {
"chrome": {"passed": 15, "total": 15, "version": "153.0.8010.48"},
"nativeCurrentParser": {"passed": 15, "total": 15},
"nativeLegacyParser": {"passed": 15, "total": 15},
"cascadeWinnerMatrix": {"passed": 17, "total": 17},
"adjacentCssom": {"passed": 21, "total": 21},
"nativeWebCtest": {"passed": 3, "total": 3},
"generatorUnitTests": {"passed": 3, "total": 3}
},
"lookupBenchmark": {
"compiler": "AppleClang 21 -O3",
"lookupsPerRun": 24000000,
"baselineMilliseconds": [507.901, 502.446, 495.039, 521.802, 495.865, 495.389, 501.778, 505.427, 506.480],
"candidateMilliseconds": [426.629, 423.418, 424.271, 434.849, 418.370, 430.056, 421.227, 440.586, 418.068],
"baselineMedianMilliseconds": 502.446,
"candidateMedianMilliseconds": 424.271,
"medianChangePercent": -15.56
},
"scaling": [
{"affected": 8, "unrelated": 32, "selectorCandidates": 16, "fallbackVisits": 0, "ruleChecks": 32, "cascadeApplications": 18, "cascadeCandidates": 32},
{"affected": 8, "unrelated": 1024, "selectorCandidates": 16, "fallbackVisits": 0, "ruleChecks": 32, "cascadeApplications": 18, "cascadeCandidates": 32},
{"affected": 128, "unrelated": 32, "selectorCandidates": 256, "fallbackVisits": 0, "ruleChecks": 512, "cascadeApplications": 258, "cascadeCandidates": 512},
{"affected": 128, "unrelated": 1024, "selectorCandidates": 256, "fallbackVisits": 0, "ruleChecks": 512, "cascadeApplications": 258, "cascadeCandidates": 512}
]
}
9 changes: 9 additions & 0 deletions experiments/WebScene.NativeEngine.Probe/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ option(WEBSCENE_NATIVE_ENGINE_BUILD_HTML_PARSER_BENCHMARK
"Build the V8-free html5ever parser microbenchmark" OFF)
option(WEBSCENE_NATIVE_ENGINE_BUILD_CSS_PARSER_BENCHMARK
"Build the V8-free selected CSS syntax parser microbenchmark" OFF)
option(WEBSCENE_NATIVE_ENGINE_BUILD_CSS_PROPERTY_METADATA_BENCHMARK
"Build the V8-free CSS property metadata lookup benchmark" OFF)
option(WEBSCENE_NATIVE_ENGINE_BUILD_DOM_BINDING_BENCHMARK
"Build the native V8 DOM binding lifecycle benchmark" OFF)
option(WEBSCENE_NATIVE_ENGINE_FRAGMENT_ATTACH_CONTROL
Expand Down Expand Up @@ -945,6 +947,13 @@ else()
message(STATUS "WebScene native engine: V8 intentionally disabled")
endif()

if(WEBSCENE_NATIVE_ENGINE_BUILD_CSS_PROPERTY_METADATA_BENCHMARK)
add_executable(webscene_css_property_metadata_benchmark
benchmarks/css_property_metadata_benchmark.cpp)
target_compile_features(webscene_css_property_metadata_benchmark PRIVATE cxx_std_20)
target_include_directories(webscene_css_property_metadata_benchmark PRIVATE native)
endif()

if(WEBSCENE_NATIVE_ENGINE_BUILD_LAYOUT_SCRATCH_BENCHMARK)
foreach(layout_benchmark_target IN ITEMS
webscene_layout_scratch_control_benchmark
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include "webscene_css_property_mask.h"

#include <array>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <string_view>

int main()
{
// Keep the mix stable: fifteen high-frequency direct fields and nine
// effective-property registry entries. This measures dispatch overhead,
// not parser or layout work.
constexpr std::array<std::string_view, 24> names{
"width", "height", "display", "position", "color", "background",
"background-color", "font-size", "font-family", "font-weight",
"line-height", "text-align", "visibility", "opacity", "transform",
"margin", "margin-left", "padding", "padding-inline-start", "gap",
"border-width", "border-top-color", "inset-inline-start", "overflow"};
constexpr size_t rounds = 1000000U;
uint64_t checksum = 0U;
const auto started = std::chrono::steady_clock::now();
for (size_t round = 0; round < rounds; ++round) {
for (const auto name : names) {
checksum ^= webscene_native::css::property_mask(name);
checksum = (checksum << 1U) | (checksum >> 63U);
}
}
const auto elapsed = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - started).count();
std::cout << "lookups=" << rounds * names.size()
<< " elapsed_ms=" << elapsed
<< " checksum=" << checksum << '\n';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"version": 1,
"properties": [
{"name": "border-block-color", "mask": ["inline_border"], "expansion": ["border-top-color", "border-bottom-color"], "applyExpansion": true},
{"name": "border-block-end-color", "mask": ["inline_border"], "alias": "border-bottom-color"},
{"name": "border-block-end-width", "mask": ["inline_border"], "alias": "border-bottom-width"},
{"name": "border-block-start-color", "mask": ["inline_border"], "alias": "border-top-color"},
{"name": "border-block-start-width", "mask": ["inline_border"], "alias": "border-top-width"},
{"name": "border-block-width", "mask": ["inline_border"], "expansion": ["border-top-width", "border-bottom-width"], "applyExpansion": true},
{"name": "border-bottom-color", "mask": ["inline_border"]},
{"name": "border-bottom-width", "mask": ["inline_border"]},
{"name": "border-color", "mask": ["inline_border"], "expansion": ["border-top-color", "border-right-color", "border-bottom-color", "border-left-color"]},
{"name": "border-inline-color", "mask": ["inline_border"], "expansion": ["border-left-color", "border-right-color"], "applyExpansion": true},
{"name": "border-inline-end-color", "mask": ["inline_border"], "alias": "border-right-color"},
{"name": "border-inline-end-width", "mask": ["inline_border"], "alias": "border-right-width"},
{"name": "border-inline-start-color", "mask": ["inline_border"], "alias": "border-left-color"},
{"name": "border-inline-start-width", "mask": ["inline_border"], "alias": "border-left-width"},
{"name": "border-inline-width", "mask": ["inline_border"], "expansion": ["border-left-width", "border-right-width"], "applyExpansion": true},
{"name": "border-left-color", "mask": ["inline_border"]},
{"name": "border-left-width", "mask": ["inline_border"]},
{"name": "border-right-color", "mask": ["inline_border"]},
{"name": "border-right-width", "mask": ["inline_border"]},
{"name": "border-top-color", "mask": ["inline_border"]},
{"name": "border-top-width", "mask": ["inline_border"]},
{"name": "border-width", "mask": ["inline_border"], "expansion": ["border-top-width", "border-right-width", "border-bottom-width", "border-left-width"]},
{"name": "bottom", "mask": ["inline_bottom"]},
{"name": "column-gap", "mask": ["inline_gap"]},
{"name": "gap", "mask": ["inline_gap"], "expansion": ["row-gap", "column-gap"]},
{"name": "inset", "mask": ["inline_top", "inline_right", "inline_bottom", "inline_left"], "expansion": ["top", "right", "bottom", "left"]},
{"name": "inset-block", "mask": ["inline_top", "inline_bottom"], "expansion": ["top", "bottom"], "applyExpansion": true},
{"name": "inset-block-end", "mask": ["inline_bottom"], "alias": "bottom"},
{"name": "inset-block-start", "mask": ["inline_top"], "alias": "top"},
{"name": "inset-inline", "mask": ["inline_left", "inline_right"], "expansion": ["left", "right"], "applyExpansion": true},
{"name": "inset-inline-end", "mask": ["inline_right"], "alias": "right"},
{"name": "inset-inline-start", "mask": ["inline_left"], "alias": "left"},
{"name": "left", "mask": ["inline_left"]},
{"name": "margin", "mask": ["inline_margin"], "expansion": ["margin-top", "margin-right", "margin-bottom", "margin-left"]},
{"name": "margin-block", "mask": ["inline_margin"], "expansion": ["margin-top", "margin-bottom"]},
{"name": "margin-block-end", "mask": ["inline_margin"], "alias": "margin-bottom"},
{"name": "margin-block-start", "mask": ["inline_margin"], "alias": "margin-top"},
{"name": "margin-bottom", "mask": ["inline_margin"]},
{"name": "margin-inline", "mask": ["inline_margin"], "expansion": ["margin-left", "margin-right"]},
{"name": "margin-inline-end", "mask": ["inline_margin"], "alias": "margin-right"},
{"name": "margin-inline-start", "mask": ["inline_margin"], "alias": "margin-left"},
{"name": "margin-left", "mask": ["inline_margin"]},
{"name": "margin-right", "mask": ["inline_margin"]},
{"name": "margin-top", "mask": ["inline_margin"]},
{"name": "overflow", "mask": ["inline_overflow"], "expansion": ["overflow-x", "overflow-y"]},
{"name": "overflow-x", "mask": ["inline_overflow"]},
{"name": "overflow-y", "mask": ["inline_overflow"]},
{"name": "padding", "mask": ["inline_padding"], "expansion": ["padding-top", "padding-right", "padding-bottom", "padding-left"]},
{"name": "padding-block", "mask": ["inline_padding"], "expansion": ["padding-top", "padding-bottom"]},
{"name": "padding-block-end", "mask": ["inline_padding"], "alias": "padding-bottom"},
{"name": "padding-block-start", "mask": ["inline_padding"], "alias": "padding-top"},
{"name": "padding-bottom", "mask": ["inline_padding"]},
{"name": "padding-inline", "mask": ["inline_padding"], "expansion": ["padding-left", "padding-right"]},
{"name": "padding-inline-end", "mask": ["inline_padding"], "alias": "padding-right"},
{"name": "padding-inline-start", "mask": ["inline_padding"], "alias": "padding-left"},
{"name": "padding-left", "mask": ["inline_padding"]},
{"name": "padding-right", "mask": ["inline_padding"]},
{"name": "padding-top", "mask": ["inline_padding"]},
{"name": "right", "mask": ["inline_right"]},
{"name": "row-gap", "mask": ["inline_gap"]},
{"name": "top", "mask": ["inline_top"]}
]
}
Loading
Loading