Skip to content

DevTools v2: timeline, selection, live CSS variables, snapshot export + stylesheet var() fixes - #18

Merged
vietnguyentuan2019 merged 13 commits into
mainfrom
feat/devtools-v2
Oct 2, 2026
Merged

vietnguyentuan2019 merged 13 commits into
mainfrom
feat/devtools-v2

Conversation

@vietnguyentuan2019

Copy link
Copy Markdown
Contributor

⚠️ Rendering behavior change: stylesheet var() now resolves

var() inside <style> / customCss rules never worked before this PR. csslib parses var() as a VarUsage node whose span lacks the var( prefix, so the resolver received "--c)" and produced nothing (fallback included). Only inline style="" worked, and that is all the existing tests covered, while the CSS matrix marked var() ✅.

  • 157a46c fixes the parsing.
  • 32e3a68 cascades custom properties before substituting var(), as CSS does. Until now a --name defined by a later, higher-specificity, inline or !important declaration on the same element was missed (e.g. p { color: var(--c) } .x { --c: blue }).

Pages that use stylesheet var() will render differently after upgrading (correctly, but visibly).

DevTools v2.x (roadmap: all 4 items)

Tab Extension Notes
Timeline getTimeline layout + paint µs per renderer (= per virtualized chunk), last 120 samples, slowest first, sparkline, 1 s live mode. Paint = canvas recording, not GPU raster.
Selection getSelection selection range + every text/ruby fragment's global [start, end), selected part highlighted, ruby text/height/position
CSS Vars getCssVariables, setCssVariable definition sites + live editing: override a --var and every HyperViewer re-resolves (debug only). A bare HyperRenderWidget with a prebuilt document is not re-resolved.
Export exportSnapshot one self-describing JSON object (tree + styles, fragments, lines, selection, timing, vars)

New core API: HyperRenderDebugHooks.onFrameTiming, onSelectionChanged and cssVariableOverrides; StyleResolver.customPropertyOverrides; extra debugFragments() fields. The timing/selection hooks only run in debug mode while a listener is set.

Also: the never-implemented "Float region visualizer" claim has been removed from ROADMAP and README.

Release order

hyper_render_core 1.10.0 → hyper_render 1.10.0 (now requires core ^1.10.0) → hyper_render_devtools (requires core ^1.10.0). Core's pubspec version is not bumped in this PR.

Verification

  • Every new extension was called over vm_service on a running macOS app; live edit went red → blue → red.
  • Root + core 2399 pass, hyper_render_html 73, hyper_render_devtools 19, devtools_ui 7 (the widget test is @TestOn('browser'): flutter test --platform chrome).
  • New tests were mutation-checked (they fail with the fix reverted), including the compute() isolate path.
  • Goldens: the same 8 pre-existing macOS font failures as main, nothing new.
  • extension/devtools/build was rebuilt by hand (devtools_extensions 0.2.2 build_and_copy passes the removed --web-renderer flag) and passes devtools_extensions validate.
  • Note: CI has a changed_devtools path filter, but no job runs the devtools tests.

- HyperRenderDebugHooks.onFrameTiming(id, phase, micros): each
  RenderHyperBox reports performLayout and paint duration. Paint is
  canvas recording time, not raster. Only timed in debug mode while the
  hook is set; release and hook-less debug builds take the direct path.
- HyperRenderDebugHooks.onSelectionChanged(id, start, end): pushed from
  paint when the selection differs from the last report. Every
  selection change repaints, so this catches them all without touching
  each selection setter.
- debugFragments() adds globalOffset, charLength, rubyText, rubyHeight
  so the inspector can show fragment boundaries in selection
  coordinates.

Both hooks are part of isActive. New tests fail if the layout report
or the change-only selection guard is removed.
New panel tabs and service extensions (devtools v2.x roadmap):

- Timeline (ext.hyperRender.getTimeline): last 120 layout/paint samples
  per renderer (= per virtualized chunk) in a ring buffer, slowest
  first, paint sparkline, 1 s live polling.
- Selection (ext.hyperRender.getSelection): selection range plus every
  text/ruby fragment's global [start, end) with the selected part
  highlighted and ruby text/height/position.
- CSS Vars (ext.hyperRender.getCssVariables): read-only definition
  sites of custom properties. Live editing is NOT done: var() is
  substituted when HyperViewer resolves styles at parse time, so it
  needs a re-resolve trigger in the root package.
- Export (ext.hyperRender.exportSnapshot): one self-describing JSON
  object; shown in a selectable dialog with a copy button, since
  clipboard access can be denied inside the DevTools iframe.

Data assembly lives in plain-Dart files (lib/src/inspector_data.dart,
devtools_ui/lib/src/inspector_logic.dart) with unit tests; a Chrome
widget test drives every tab in demo mode. devtools_ui/test was
gitignored as create-boilerplate; un-ignored for these tests.

Requires hyper_render_core ^1.10.0 (the new hooks). Extension rebuilt
and validated. ROADMAP: tick 3 of 4, mark CSS vars half done, and
remove the never-implemented 'Float region visualizer' claim.
- RenderHyperBox.attach(): DevTools forgets a renderer's selection on
  detach, so a box re-attached with its selection intact (GlobalKey
  reparent, keep-alive) must re-report it. Regression test fails
  without the reset.
- demo_mode_test is @teston('browser'): the panel pulls in
  dart:js_interop, so a plain VM 'flutter test' skips it instead of
  failing to load.
- README: drop the 'float visualizer' claim (it never existed).
csslib parses var() as a VarUsage node whose span is only "--name)" /
"--name, fallback)" (the same quirk the FunctionTerm branch already
works around). _convertRuleSet kept that raw span, so the resolver saw
the value "--c)" and every var() in <style> / customCss produced
nothing, fallback included. Only inline style="" (a separate parser)
worked, and that is all the existing tests covered, while the CSS
matrix marked var() as supported.

Rebuild it as "var(" + span. Covers :root inheritance, fallback,
lengths in multi-term shorthands and nested var() fallbacks; all 4
new tests fail without the fix.
Override a --var from the DevTools CSS Vars tab and every HyperViewer
re-resolves its styles.

- core: StyleResolver.customPropertyOverrides replaces a custom
  property's value at its single cascade write site, so every var()
  read afterwards sees it, including one later in the same rule. A
  trailing '* { --x: v !important }' cannot do that: var() is
  substituted in the normal pass, before !important runs.
- core: HyperRenderDebugHooks.cssVariableOverrides (ValueNotifier).
- root: in debug builds HyperViewer listens and re-parses, passing the
  overrides to all three resolver sites, including the compute()
  isolate via the args record (statics are not shared across
  isolates). No-op in release.
- devtools: ext.hyperRender.setCssVariable (empty value removes,
  '*' clears all, non --names -> invalidParams); getCssVariables also
  returns the active overrides. Panel: edit / undo per row, 'Reset N
  overrides', override badge. The edit dialog owns its controller:
  disposing it right after showDialog returned crashed the closing
  TextField (caught by the demo widget test).

Verified live: a HyperViewer with :root { --brand: red } goes
red -> blue -> red through setCssVariable over vm_service. Root tests
cover sync and virtualized (async) modes and fail if the listener
does not re-parse. Extension rebuilt and validated.
var() was substituted while declarations were applied, so a --name
defined by a later, higher-specificity, inline or !important
declaration on the same element was missed, e.g.
'p { color: var(--c) } .x { --c: blue }' read the parent's --c. This
was hidden while stylesheet var() did not resolve at all (157a46c).

Compute the element's final custom properties first (rules by
specificity, inline, !important, with overrides applied), seed the
style with them, and make every --name write during the cascade store
that final value, so no var() observes an intermediate one. Skipped
entirely unless a rule or the inline style declares a custom property.
Four new tests fail without the pre-pass; the 8 golden failures are
the same set as on main (macOS fonts).

Also:
- root requires hyper_render_core ^1.10.0: HyperViewer now uses
  StyleResolver.customPropertyOverrides and
  HyperRenderDebugHooks.cssVariableOverrides, so a 1.9.x core cannot
  compile it (the v1.7.1 lower-bound defect).
- test the compute() isolate parse path (useMicrotaskParsing: false,
  runAsync): fails if the isolate resolver drops the overrides.
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

✅ Layout Regression — All fixtures within 60 FPS budget

Fixture Budget (ms) Median (ms) P95 (ms)
❌ simple_paragraph 8 24 31
❌ mixed_inline 10 20 23
❌ float_layout 12 17 33
❌ table_20_rows 14 50 73
❌ cjk_ruby 14 15 23
❌ large_article 16 39 64

One or more fixtures exceeded the 16 ms budget.

Flutter 3.41.5 · ubuntu-22.04

No action required.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

❌ Visual Regression Detected

27 golden test(s) failed on Flutter 3.41.5 / ubuntu-22.04.

The rendered output no longer matches the reference images.

If the change is intentional, regenerate the goldens on the
same platform via the Update Goldens workflow dispatch, or run
locally in Docker:

docker run --rm \
  -v $(pwd):/workspace -w /workspace \
  ghcr.io/cirruslabs/flutter:3.41.5 \
  bash -c "apt-get update -qq && \
    apt-get install -y fonts-noto fonts-noto-cjk fonts-roboto && \
    flutter pub get && \
    flutter test test/golden/ --update-goldens"
git add test/golden/goldens/
git commit -m "chore: update golden references (Flutter 3.41.5)"

⚠️ Always regenerate goldens on ubuntu-22.04 with Flutter
3.41.5
to keep references pixel-stable across machines.

…hing

Making stylesheet var() work (157a46c) made untrusted <style> CSS
(extracted before sanitization) live, so this audits that surface:

- var() expansion is bounded: checked per substitution at 16 KB. A
  10-refs x 10-levels --l chain would otherwise expand to ~10^10
  chars from untrusted HTML. Over-long or empty results are invalid
  and the declaration is ignored, not applied empty.
- url() and calc()/min()/max()/clamp() had the same csslib span quirk
  as var() (UriTerm/CalcTerm drop the function name), so stylesheet
  background-image: url() and width: calc() never worked either. A
  sweep of 50 value shapes found no other affected term types.
- background/background-image URLs go through UrlSafety.isSafe (the
  <img src> policy). Nothing paints backgroundImage today, but it
  must not hold javascript:/file:/svg URLs once something does.
- ':root' was unknown to the pseudo-class matcher and fell through to
  'match', so it applied to EVERY element: :root { font-size: 125% }
  compounded per level, and :root { --c } reset descendant overrides.
- Perf: var() lookup no longer merges local+inherited maps per
  declaration, rules cache whether they declare custom properties,
  and elements without their own custom properties share the
  parent's map. 300 :root vars x 2000 elements: 457 ms -> 37 ms.

Tests: core stylesheet_var_security_test (bomb, cycles, 6 blocked
schemes x sheet/var/inline), stylesheet_var_performance_test (ratio
with vs without vars; ~15x before, ~1.4x now), :root and url/calc
regressions; root HyperViewer-level <style> bomb + scheme tests and
var/url/calc fuzz seeds. Each new test fails with its fix reverted.
- Pre-flight: root 'flutter analyze' reached into html/markdown/
  highlight/math/epub, which only resolve against their own pubspec
  (200 uri_does_not_exist errors in a clean checkout). Exclude every
  sub-package except core from the root analysis and analyze each one
  in place after its own pub get.
- Deploy Live Web Demo: Pages serves the gh-pages branch, but the job
  used actions/deploy-pages, whose github-pages environment does not
  allow main, so every run failed and the demo stayed on v1.7.1. Build
  and push to gh-pages instead (no repo settings change).
- Core Validation: sub-package steps ran 'flutter test --no-pub' in
  directories that were never resolved; pub get first. Add the
  missing math, epub, devtools and devtools_ui (--platform chrome)
  test steps and their path filters.
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

✅ Layout Regression — All fixtures within 60 FPS budget

Fixture Budget (ms) Median (ms) P95 (ms)
❌ simple_paragraph 8 12 24
❌ mixed_inline 10 11 14
✅ float_layout 12 11 16
❌ table_20_rows 14 29 49
✅ cjk_ruby 14 7 8
❌ large_article 16 26 42

One or more fixtures exceeded the 16 ms budget.

Flutter 3.41.5 · ubuntu-22.04

No action required.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

❌ Visual Regression Detected

27 golden test(s) failed on Flutter 3.41.5 / ubuntu-22.04.

The rendered output no longer matches the reference images.

If the change is intentional, regenerate the goldens on the
same platform via the Update Goldens workflow dispatch, or run
locally in Docker:

docker run --rm \
  -v $(pwd):/workspace -w /workspace \
  ghcr.io/cirruslabs/flutter:3.41.5 \
  bash -c "apt-get update -qq && \
    apt-get install -y fonts-noto fonts-noto-cjk fonts-roboto && \
    flutter pub get && \
    flutter test test/golden/ --update-goldens"
git add test/golden/goldens/
git commit -m "chore: update golden references (Flutter 3.41.5)"

⚠️ Always regenerate goldens on ubuntu-22.04 with Flutter
3.41.5
to keep references pixel-stable across machines.

github-actions Bot and others added 3 commits October 2, 2026 00:23
…s they found

- HtmlToSpanConverter (0% covered): text/inline/block/break, links +
  recognizer disposal, display:none, image/media/formula/placeholder
  atoms, ruby, and all three build helpers. Found: whitespace
  normalization used Dart's \s, collapsing &nbsp; (U+00A0) — now the
  CSS whitespace set (cssWhitespaceRun), like every other call site.
- FormulaWidget (0% covered): Greek, prefix-collision cases (\in vs
  \infty/\int, \le vs \leq), frac/sqrt, scripts, customBuilder,
  FormulaParser. Found: x^{2} / a_{i} (the usual LaTeX spelling) were
  left as x^2 — braces were stripped only after the script tables ran.
- Paint paths (render_hyper_box_paint was 48%): rasterized pixel
  checks for solid/dashed/dotted/double borders (+ dashed < solid),
  block background + radius, inline background/border, debug bounds
  (absent without the flag), selection highlight (before/after) and a
  float placeholder. Notes in the file on why the Ahem test font needs
  padding / transparent text, and why counts use straight RGBA.
…s sync

Versions (only packages with changes since their last release move):
- hyper_render_core 1.9.0 -> 1.10.0 (new debug hooks, stylesheet
  var()/url()/calc() fixes, :root, perf)
- hyper_render 1.9.1 -> 1.10.0 (requires core ^1.10.0)
- hyper_render_devtools 1.7.0 -> 1.8.0 (requires core ^1.10.0)
- html/markdown/highlight/math/clipboard/epub: no code change, stay.

Docs: LIMITATIONS claimed background-image url() as supported (matrix
and code say nothing paints it); CSS matrix var()/calc() rows now say
stylesheet resolution works as of 1.10.0 and min/max/clamp are not
evaluated; MIGRATION_GUIDE gets an 'Upgrading to v1.10.0' section on
the rendering changes; root ROADMAP milestone table was stale (v1.9.0
listed as diagramming, v1.7-1.8 as 'Current/Q3'); install snippets and
CHANGELOG headings carry the new versions.

Demo: integration_test/all_demos_test.dart drives the real macOS app
through all 27 home demos and their hub sub-pages, failing on any
FlutterError (including RenderFlex overflow, which the widget-test
smoke net ignores). macOS deployment target raised 10.15 -> 12.0
(Podfile + Runner) — Xcode 26 rejects lower.
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

✅ Layout Regression — All fixtures within 60 FPS budget

Fixture Budget (ms) Median (ms) P95 (ms)
❌ simple_paragraph 8 16 29
❌ mixed_inline 10 14 15
❌ float_layout 12 13 28
❌ table_20_rows 14 43 64
✅ cjk_ruby 14 10 11
❌ large_article 16 37 63

One or more fixtures exceeded the 16 ms budget.

Flutter 3.41.5 · ubuntu-22.04

No action required.

It ran 'flutter test test/integration/' with no device target, i.e. on
the host VM: the same tests test-pr already runs on Linux. Its Android
leg could not boot an x86 emulator on Apple-Silicon macos-latest
(HVF error: HV_UNSUPPORTED) and both legs bill at the macOS rate (~10x
Linux). Real on-device coverage is example/integration_test/
all_demos_test.dart, run locally with -d macos.
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

✅ Layout Regression — All fixtures within 60 FPS budget

Fixture Budget (ms) Median (ms) P95 (ms)
❌ simple_paragraph 8 19 27
❌ mixed_inline 10 16 16
❌ float_layout 12 15 31
❌ table_20_rows 14 46 72
✅ cjk_ruby 14 11 14
❌ large_article 16 38 63

One or more fixtures exceeded the 16 ms budget.

Flutter 3.41.5 · ubuntu-22.04

No action required.

@vietnguyentuan2019
vietnguyentuan2019 merged commit ecafa19 into main Oct 2, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant