Skip to content

feat(compose): generic switchable trace for lazy layout measure - #121

Open
bytemain wants to merge 192 commits into
mainfrom
cc/lazy-layout-trace
Open

feat(compose): generic switchable trace for lazy layout measure#121
bytemain wants to merge 192 commits into
mainfrom
cc/lazy-layout-trace

Conversation

@bytemain

@bytemain bytemain commented Aug 5, 2026

Copy link
Copy Markdown
Member

Diagnostics seam inside the lazy list measure pass, for task Tencent-TDS#990 (Thread IME shrink white screen).

Review boundary — read this instead of the main diff. GitHub compares against main, which carries a large unrelated staging2 divergence. The actual change is three files between c7a02127 (the commit the mobile carrier pins) and b751f14e:

compose/.../diagnostics/LazyLayoutTrace.kt      +210   new
compose/.../foundation/lazy/LazyList.kt          +16/-1
compose/.../foundation/lazy/LazyListMeasure.kt   +57/-1

git diff c7a02127 b751f14e -- compose/src/commonMain/kotlin/com/tencent/kuikly/compose/

Why in the library

A host reading LazyListState.layoutInfo sees only a snapshot after the pass. In the incident under investigation every consumer-visible counter reads identically between a fully white frame and a healthy one — same descendant counts, same draw count, same visible range. Which measure or placement first diverged is only answerable from inside the measure pass.

Generic by construction

No product vocabulary enters the library. The host supplies opaque traceSession / cycle / layoutGeneration / targetToken; they are compared for equality so records can be joined, and never parsed, persisted, or interpreted. The sink is injected by the caller and released with its composition, so this never becomes a second owner of any state.

Off means off

ENABLED is a compile-time constant. measure { } is inline with the check ahead of the lambda, so a disabled build gathers no geometry, traverses no list, and formats no string.

This branch sets it true because it is the forensics carrier; main keeps it false, and that constant is the one line a build flips. An earlier revision of this branch left it false and would have installed, run, and recorded nothing — a package that compiles green and traces nothing is the failure this work exists to expose, so it is called out rather than buried.

Fails closed rather than looking like data

With tracing enabled, a missing frame identity raises LazyTraceWiringError instead of substituting (-1, -1). A placeholder frame yields records that cannot be joined but still resemble data, which is worse than an absent record.

Coverage is unioned, not summed

lazyTraceViewportCoverage unions clipped spans. Summing would double-count sticky or overlapping items and report a viewport as fuller than it is — concealing the empty band the measurement exists to find.

Not included

LazyTraceStage declares Placement and NativeCommit; only MeasureResult is emitted. There are no library tests: this module has no test source set (compose/src has no commonTest, and build.gradle.kts declares no test dependencies). Adding that infrastructure changes the fork's build and affects upstream absorption audits, so it is left to the repo owner. Carrier-side behaviour tests exercising this seam are being added on the mobile side.

Merge status

Consumed by a forensics gitlink only — the mobile carrier pins this branch commit directly so a diagnostic package can be built without waiting on staging2. Per the repo rule, a publishable build must point at a staging2 commit; this SHA must not enter a release artifact.

bytemain and others added 30 commits June 18, 2026 22:12
V2 styled string nodes are rendered by ArkUI Text internally. Registering the legacy foreground-draw callback for those nodes can route them through OH_Drawing_TypographyPaint before the paragraph is layout-ready, which spams paragraph-is-not-formatted warnings.

Track the styled-string path on KRRichTextView, unregister/skip foreground draw for that path, and reset styled-string state when falling back to the legacy typography path.
Throttle Compose↔Native scroll sync (calc/expand deferral, contentSize
dedup, scrollEnd finalize), quantize ArkUI onScroll in fling, and draw
RichText when typography is ready during main-thread tasks.

Co-authored-by: Cursor <cursoragent@cursor.com>
Quantize ArkUI scroll callbacks and SubcomposeLayout deltas at 0.5px,
dedupe content size updates, and flush the final offset on scroll stop.
Add OHOS demo cold-start page params and a CanScrollForward repro page.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ess test

Add a 500-item repeated demo list with Card UI and inspector enabled to reproduce and profile HarmonyOS scroll performance locally.

Co-authored-by: Cursor <cursoragent@cursor.com>
Clamp ArkUI scroll-frame-begin offset at nested list edges and skip
compose-side expand/offset correction when nestedScroll is configured.

Co-authored-by: Cursor <cursoragent@cursor.com>
Only recalculate native content size when contentView main-axis height
changes during fling, avoiding ~650 redundant calc calls per gesture
while keeping near-bottom and scrollEnd sync unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
Extend KuiklyScrollTrace with remeasure, frame timing, and offset/drag
skip metrics for OHOS scroll stress validation; gate redundant
contentOffset and isDragging writes; skip resetViewVisible when unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
bytemain and others added 18 commits August 3, 2026 04:09
Wake a retained Kuikly subcompose slot's draw ownership when it is successfully restored from the reusable pool, allowing hidden native descendants to flush visible on the first reactivated frame.

Cover same-key and compatible-key reuse, consecutive reuse cycles, and the already-active no-op path through the real precompose/dispose/retrieve state machine.

Signed-off-by: MingQi <raft-mobile-mingqi@mail.build>
Wake draw ancestry when a precomposed slot becomes active through real subcompose consumption.

Signed-off-by: MingQi <raft-mobile-mingqi@mail.build>
Dirty restored lazy descendants and force their draw ancestry after offscreen visibility is restored. Cover the real native mount, placement, restore, and clean-leaf lifecycle with production-callsite mutation teeth.

Signed-off-by: MingQi <raft-mobile-mingqi@mail.build>
Preserve child content for bare draw nodes, route view-aware fallback through ordinary draw overrides, and retain explicit view-aware precedence.

Add behavior coverage for all three implementer classes and a self-testing production-wiring constraint for LayoutNodeDrawScope.drawDirect.

Signed-off-by: MingQi <raft-mobile-mingqi@mail.build>
Signed-off-by: BiSheng <raft-mobile-bisheng@mail.build>
Co-authored-by: BiSheng <raft-mobile-bisheng@mail.build>
Task Tencent-TDS#183: synchronize Compose offset after native viewport changes.

Reviewed exact: 175c8ac
Independent review: task Tencent-TDS#205 FINAL FULL-SEAT PASS / 0 blocker.
Hosted exact matrix: 30852685305.

Signed-off-by: MingQi <mingqi@mail.build>
Signed-off-by: Android-Developer-1 <raft-mobile-android-developer-1@mail.build>
Co-authored-by: Android-Developer-1 <raft-mobile-android-developer-1@mail.build>
Signed-off-by: Jony <jony@mail.build>
Co-authored-by: Jony <jony@mail.build>
Signed-off-by: KMP-专家 <raft-mobile-kmp-expert@mail.build>
Co-authored-by: KMP-专家 <raft-mobile-kmp-expert@mail.build>
Keep the active programmatic scroll target aligned with the native scroller when a viewport shrink commits a smaller frame. Preserve owner fencing across synchronous native echoes, lock the behavior with a renderer-level regression fixture, and remove the obsolete source-code assertion gate.

Signed-off-by: MingQi <mingqi@mail.build>
Adds a diagnostics seam inside the lazy list measure pass, for a host that
needs to know which measure or placement first diverged from what the user
saw. A consumer reading LazyListState.layoutInfo only sees a snapshot after
the fact and cannot answer that.

Generic by construction: no product vocabulary enters the library. The host
supplies opaque traceSession/cycle/layoutGeneration/targetToken values, which
are compared for equality so records can be joined and never parsed,
persisted, or interpreted. The sink is injected by the caller and released
with its composition, so this never becomes a second owner of anything.

Switched by a compile-time constant rather than a runtime flag, so a disabled
build can be shown to do no work rather than merely to stay quiet: the
measure lambda is inline and the check precedes it, so no geometry is
gathered, no list is traversed and no string is built when tracing is off.
requireConsistentWith fails a build whose host and library switches disagree,
since that yields a chain silently missing one layer.

Viewport coverage unions clipped item spans instead of summing them, so
sticky or overlapping items cannot report a viewport as fuller than it is and
hide the empty band the measurement exists to expose.

measureLazyList takes the handle as a defaulted trailing parameter, so its
existing caller is unchanged and ordinary builds pass null.

Signed-off-by: CC-希乐 <raft-mobile-cc-xile@mail.build>
Without this the seam added in the previous commit could never fire: the sole
caller of measureLazyList passed nothing, so the defaulted parameter stayed
null and no record was ever produced.

The handle now reaches the measure policy through a static composition local,
read once where the policy is remembered rather than inside the measure lambda
— so the measure path performs no lookup, and a host that provides nothing
still pays nothing. Scoped to the provider's composition, so the handle is
released with the screen that supplied it instead of persisting as global
state.

Signed-off-by: CC-希乐 <raft-mobile-cc-xile@mail.build>
Two wiring gaps @mingqi found by reading the commit rather than the
description, either of which produces a package that compiles green and
traces nothing.

ENABLED was hardcoded false with no build wiring, so the inline guard
returned before the lambda and no record was ever produced — the exact
"looks healthy, contains nothing" failure this work exists to expose. This
forensics branch sets it true; main keeps it false, and that constant is the
single line a build flips.

The frame identity fell back to (-1, -1) when unsupplied, which yields
records that cannot be joined but still look like data. With tracing enabled
a missing frame is now a wiring error, and LazyList supplies a real identity:
a monotonic sequence and clock reading, allocated only when a host actually
provided a trace.

Signed-off-by: CC-希乐 <raft-mobile-cc-xile@mail.build>
Found by @mingqi. The remembered measure lambda did not list the trace handle
among its keys, so it closed over the first one permanently. The handle
carries the host's correlation identity — which changes per IME cycle and
layout generation — so every later record would be attributed to a stale
cycle: records that look valid and join to the wrong pass, which is worse
than no records.

Signed-off-by: CC-希乐 <raft-mobile-cc-xile@mail.build>
Measure records where items should go; this records that placement actually
ran for that frame, with the target's placed offset and size and the resulting
viewport coverage.

Without it the interval between "measure geometry was correct" and "the pixels
were white" is a blind spot crossable only by assuming adjacent timestamps
belong to the same frame — an inference already retracted once here. Placement
carries the same frame identity as the measure record.

Signed-off-by: CC-希乐 <raft-mobile-cc-xile@mail.build>
@mingqi read the exact and found the record sat at the top of the layout
block, before any it.place() ran — it merely re-reported measure's
positionedItems and proved nothing about placement. It now emits after the
item and header placement calls and after attachToScope, so it describes work
that has actually happened.

Signed-off-by: CC-希乐 <raft-mobile-cc-xile@mail.build>
On a viewport shrink this path corrects only the Compose offset; nothing in
it re-drives the native scroller. Whether the landed replay fix could apply
at all depends on a pending programmatic offset existing at that moment, and
that is not decidable by reading — the value is written on only two paths,
each with an early return.

Records the shrink direction, both offsets, content size, the pending
programmatic offset and the dirty flag, plus what the Compose offset was
corrected to and what the native offset was left at. Behind the same
compile-time switch, so ordinary builds are unaffected.

Signed-off-by: CC-希乐 <raft-mobile-cc-xile@mail.build>
Completes the shrink diagnostic per @mingqi. Recording only the shrink moment
would leave a null pending offset ambiguous between never installed and
already consumed by an earlier native echo — the same silence-versus-negative
trap this investigation has hit repeatedly.

Adds the install point in applyOffsetDelta (whose early return means an
unchanged offset installs nothing), the consume point in
consumeIgnoredScrollOffset (which clears whether or not it matched), and both
outcomes of #117's replay guard, so a skipped replay is visible rather than
silent. All behind the same compile-time switch.

Signed-off-by: CC-希乐 <raft-mobile-cc-xile@mail.build>
@bytemain
bytemain force-pushed the cc/lazy-layout-trace branch from d9f0cfa to 7271a4d Compare August 5, 2026 18:15
bytemain pushed a commit that referenced this pull request Aug 18, 2026
Task #141 follow-up to task #93. Adds the frozen 45-file closure for
org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0-KBA-002 and its
available-at kotlinx-coroutines-test-ohosarm64 module, plus a Hosted
carrier that validates the contract and resolves the authority bytes
from an empty cache.

The file set is empirical: the authority publishes no directory listing,
so it was derived from Gradle module metadata and then probed. Neither
coordinate ships a .spdx.json, and only the OHOS module has a javadoc
jar, so this closure is not shaped like task #121/#127.

The resolver asserts module metadata only for the OHOS platform module
and checks the root as artifact bytes. Resolving the multiplatform root
on a JVM runner selects jvmApiElements-published, which redirects via
available-at to kotlinx-coroutines-test-jvm, a platform this task does
not mirror; that redirect is a property of the resolving host, not of
the frozen bytes. The run prints that the mirrored root resolves for
OHOS targets only.

atomicfu 0.23.2-KBA-001 and kotlinx-coroutines-core 1.8.0-KBA-002 are
already public. kotlin-stdlib 2.0.21-KBA-001 is absent from Raft but is
a pre-existing condition of the already-published and already-consumed
coroutines-core-ohosarm64, so it is recorded as a conditional closure
member and is not published by this task.

This commit publishes nothing. Plan is ALL_ABSENT 45/45, 0 conflict.

Signed-off-by: BiSheng <raft-mobile-bisheng@mail.build>
bytemain pushed a commit that referenced this pull request Aug 18, 2026
Task #141 follow-up to task #93. Adds the frozen 45-file closure for
org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0-KBA-002 and its
available-at kotlinx-coroutines-test-ohosarm64 module, plus a Hosted
carrier that validates the contract and resolves the authority bytes
from an empty cache.

The file set is empirical: the authority publishes no directory listing,
so it was derived from Gradle module metadata and then probed. Neither
coordinate ships a .spdx.json, and only the OHOS module has a javadoc
jar, so this closure is not shaped like task #121/#127.

The authority actually supplies 50 files for these two coordinates, not
45. The difference is the root -kotlin-tooling-metadata.json and its 4
checksum sidecars, which take no part in dependency resolution and are
absent from Raft for the already-published sibling family
kotlinx-coroutines-core 1.8.0-KBA-002 and atomicfu 0.23.2-KBA-001. This
task keeps that family shape and mirrors 45, but the gap is now named in
the manifest and bound by contract as authority_files=50, because a
create-only Maven write can never be yanked. The 45-vs-50 split was
measured by a 228-probe suffix sweep, and the probe grid is recorded in
the manifest so the next coordinate does not depend on recall.

The resolver asserts module metadata only for the OHOS platform module
and checks the root as artifact bytes. Resolving the multiplatform root
on a JVM runner selects jvmApiElements-published, which redirects via
available-at to kotlinx-coroutines-test-jvm, a platform this task does
not mirror; that redirect is a property of the resolving host, not of
the frozen bytes. The run prints that the mirrored root resolves for
OHOS targets only.

atomicfu 0.23.2-KBA-001 and kotlinx-coroutines-core 1.8.0-KBA-002 are
already public. kotlin-stdlib 2.0.21-KBA-001 is absent from Raft but is
a pre-existing condition of the already-published and already-consumed
coroutines-core-ohosarm64, so it is recorded as a conditional closure
member and is not published by this task.

This commit publishes nothing. Plan is ALL_ABSENT 45/45, 0 conflict.

Signed-off-by: BiSheng <raft-mobile-bisheng@mail.build>
bytemain pushed a commit that referenced this pull request Aug 19, 2026
artin asked that the publication process land in the repo so the next one is
easier. Tasks #120/#121/#127/#141 each grew a near-duplicate workflow and the
knowledge stayed in threads.

Captures the traps that actually cost rounds, rather than restating the happy
path: the authority serves no directory listing and answers a directory URL
with 200 plus a not-browseable notice rather than 404; Raft answers some absent
paths with 401 so 404 is not the only absent signal; Raft serves no
maven-metadata.xml so only exact versions resolve; carriers must be exactly one
commit or identity-diff fails; and expected_sha must be the landed staging3
tip, because squash-merging means the reviewed head is not on staging3.

Adds stage_manifest.py, which materialises a frozen manifest into a local
Maven layout with per-byte verification so a consumer can be exercised before
an unyankable write.

It deliberately ships no consumer-side version check. Such a check reads a
build file this repository does not own, so it could not run here; shipping it
would imply coverage that does not exist. The consumer contract belongs to the
consuming repository, and the runbook says so.

The 1.7.3 trap is documented with measurements rather than speculation:
kotlinx-coroutines-test:1.7.3 is absent from Tencent and Raft but present on
Maven Central, while kotlinx-coroutines-test-ohosarm64:1.7.3 exists nowhere at
all, because upstream never published an OHOS target. Copying that version
therefore fails loudly at variant resolution instead of silently falling back.

Suite is 22/22.

Signed-off-by: BiSheng <raft-mobile-bisheng@mail.build>
bytemain pushed a commit that referenced this pull request Aug 19, 2026
artin asked that the publication process land in the repo so the next one is
easier. Tasks #120/#121/#127/#141 each grew a near-duplicate workflow and the
knowledge stayed in threads.

Captures the traps that actually cost rounds, rather than restating the happy
path: the authority serves no directory listing and answers a directory URL
with 200 plus a not-browseable notice rather than 404; Raft answers some absent
paths with 401 so 404 is not the only absent signal; Raft serves no
maven-metadata.xml so only exact versions resolve; carriers must be exactly one
commit or identity-diff fails; and expected_sha must be the landed staging3
tip, because squash-merging means the reviewed head is not on staging3.

Adds stage_manifest.py, which materialises a frozen manifest into a local
Maven layout with per-byte verification so a consumer can be exercised before
an unyankable write.

It deliberately ships no consumer-side version check. Such a check reads a
build file this repository does not own, so it could not run here; shipping it
would imply coverage that does not exist. The consumer contract belongs to the
consuming repository, and the runbook says so.

The 1.7.3 trap is documented with measurements rather than speculation:
kotlinx-coroutines-test:1.7.3 is absent from Tencent and Raft but present on
Maven Central, while kotlinx-coroutines-test-ohosarm64:1.7.3 exists nowhere at
all, because upstream never published an OHOS target. Copying that version
therefore fails loudly at variant resolution instead of silently falling back.

Suite is 22/22.

Signed-off-by: BiSheng <raft-mobile-bisheng@mail.build>
bytemain pushed a commit that referenced this pull request Aug 19, 2026
artin asked that the publication process land in the repo so the next one is
easier. Tasks #120/#121/#127/#141 each grew a near-duplicate workflow and the
knowledge stayed in threads.

Captures the traps that actually cost rounds, rather than restating the happy
path: the authority serves no directory listing and answers a directory URL
with 200 plus a not-browseable notice rather than 404; Raft answers some absent
paths with 401 so 404 is not the only absent signal; Raft serves no
maven-metadata.xml so only exact versions resolve; carriers must be exactly one
commit or identity-diff fails; and expected_sha must be the landed staging3
tip, because squash-merging means the reviewed head is not on staging3.

Two enumeration habits are recorded as actions rather than maxims: keep the
probe grid in the manifest header so the next coordinate runs a known grid, and
pair every zero-hit claim with a known-present positive control, since a bare
zero cannot distinguish absence from a filter aimed at the wrong thing.

Adds stage_manifest.py, which materialises a frozen manifest into a local
Maven layout with per-byte verification so a consumer can be exercised before
an unyankable write.

It deliberately ships no consumer-side version check. Such a check reads a
build file this repository does not own, so it could not run here; shipping it
would imply coverage that does not exist. The consumer contract belongs to the
consuming repository, and the runbook says so.

The 1.7.3 trap is documented with measurements rather than speculation:
kotlinx-coroutines-test:1.7.3 is absent from Tencent and Raft but present on
Maven Central, while kotlinx-coroutines-test-ohosarm64:1.7.3 exists nowhere at
all, because upstream never published an OHOS target. Copying that version
therefore fails loudly at variant resolution instead of silently falling back.

Every stage_manifest failure path is covered by tests that go red when the
exception type is removed. Without them the first draft shipped five raise
sites referencing an undefined name, so the tool would have reported itself as
broken at the exact moment it caught a real digest mismatch.

Suite is 24/24.

Signed-off-by: BiSheng <raft-mobile-bisheng@mail.build>
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