feat(compose): generic switchable trace for lazy layout measure - #121
Open
bytemain wants to merge 192 commits into
Open
feat(compose): generic switchable trace for lazy layout measure#121bytemain wants to merge 192 commits into
bytemain wants to merge 192 commits into
Conversation
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>
Signed-off-by: Jony <raft-mobile-jony@mail.build> Co-authored-by: Jony <raft-mobile-jony@mail.build>
Signed-off-by: Android-Developer-4 <raft-mobile-android-developer-4@mail.build> Co-authored-by: Android-Developer-4 <raft-mobile-android-developer-4@mail.build>
…#96) Add SelectableText, a read-only plain text component rendered by each platform's native text view so the OS selection experience is available anchored to the selection. Baseline guarantee: word selection, drag handles, Select all, Copy; further menu actions (Translate / Look Up / Share / Android PROCESS_TEXT targets) appear only as provided by the OS version, locale and installed services. The surface never opens an IME and text mutates only via the text prop. - core: SelectableTextView (KRSelectableTextView type) measured with the shared rich-text shadow, SelectableTextAttr reusing TextConst wire keys, ViewContainer.SelectableText DSL - Android: KRSelectableTextView (TextView + setTextIsSelectable), system ActionMode; registered in KuiklyRenderViewBaseDelegator - iOS: KRSelectableTextView (UITextView editable=NO selectable=YES), system edit menu; resolved by class-name convention - OHOS: KRSelectableTextView (ARKUI_NODE_TEXT + NODE_TEXT_COPY_OPTION local-device copy scope), native selection menu; registered in ComponentsRegisterEntry - Compose: SelectableText(text, modifier, style) with a pure style-to-props resolver; every supported field always resolves to a concrete wire value (deterministic defaults for unspecified fields) so style updates on the reusable node reset prior values on both the native renderer and the measuring shadow - tests: Robolectric behavior contract (selectable/read-only/wire props) and commonTest teeth covering resolver defaults, density scaling, weight/align mapping, full wire coverage and the specified->Default reset sequence - docs: docs/API/components/selectable-text.md with explicit capability boundary (baseline Select all/Copy; extras are platform-provided) Raft: #Kuiklybase task #69 (Phase 1 of #proj-chat task Tencent-TDS#924) Signed-off-by: BiSheng <raft-mobile-bisheng@mail.build> Co-authored-by: BiSheng <raft-mobile-bisheng@mail.build> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: artin <artin@cat.ms>
Signed-off-by: Android-Developer-4 <raft-mobile-android-developer-4@mail.build> Co-authored-by: Android-Developer-4 <raft-mobile-android-developer-4@mail.build>
Signed-off-by: Argus <argus@mail.build> Co-authored-by: Argus <argus@mail.build>
Signed-off-by: artin <artin@cat.ms>
Two independent defects starved the system selection experience on the first real Android consumer (task #69 runtime reopen): Chain B - native touch starvation: under a compose-hosted root, any hit path containing Modifier.nativeDispatchCapture() (overlay/scrim barriers) made the render root withhold all MotionEvents from native children, so the selectable TextView never received the gesture. Native dispatch resolution is now a three-state, branch-scoped policy (INHERIT / CAPTURE / RELEASE): the ancestor stance flows top-down per branch before sibling reduction, each root-to-leaf path resolves to its deepest non-INHERIT stance, and sibling branches combine with any-capture-wins. A RELEASE region therefore only neutralizes capture ancestors on its own path - including under shared hit-path prefixes, where a capture ancestor keeps capturing for every branch that does not release it itself. RELEASE is expressed through an internal marker node (no public ABI change: PointerInputModifierNode keeps only the existing captureNativeDispatch contract, and the policy enum is internal). SelectableText carries the internal nativeDispatchRelease modifier so its region keeps native delivery while barriers still block click-through everywhere else. The root latch stays gesture-sticky at ACTION_DOWN (existing KRView behavior). Chain A - a11y mask: the compose semantics bridge derives an accessibilityInfo clickable/long-clickable mask from compose click semantics, which this surface does not have, so the delegate reported the view as not long-clickable (breaking automation readouts and a11y ACTION_LONG_CLICK) and the iOS setter would overwrite the text view's native traits. Both platform views decline exactly this mask as a view capability, and the Android view corrects the final node info from its real flags in onInitializeAccessibilityNodeInfo, advertising ACTION_LONG_CLICK and ACTION_SET_SELECTION; every other a11y prop (role, testTag, plain text, state description) still applies. Teeth: policy-tree commonTest executing the production resolveNativeDispatchPolicyTree traversal (shared with HitPathTracker, not mirrored) driven by the real capture/release modifier nodes over branch trees (deep release over own ancestor, shared-capture-ancestor fan-out keeps capturing for non-release branches, sibling independence, symmetric deep capture, release-alone no-capture, same-node dominance); KRViewSuperTouchDispatchTest wiring the real superTouch root to a real native child via a non-invasive touch listener - capture withholds the entire stream (fails on the pre-fix framework), the decision is ACTION_DOWN-sticky, and release delivers the full stream with system word selection starting; a11y teeth cover both prop orders, assert truthful node info with selection actions, and certify the delegate-owned props (viewIdResourceName, mask-decline) by driving the real production delegate directly because Robolectric's createAccessibilityNodeInfo does not route through attached delegates - real uiautomator testTag/long-clickable readouts remain in the fresh Alpha blind-test contract (dispatch/state certification only - real ActionMode stays a device gate). Raft: #Kuiklybase task #69 (runtime reopen; consumer task Tencent-TDS#924) Signed-off-by: BiSheng <raft-mobile-bisheng@mail.build> Co-authored-by: BiSheng <raft-mobile-bisheng@mail.build> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…n model, not -inset.top (#102) OHOS realizes contentInset with an ArkUI margin, so the held refresh resting offset must clamp to zero rather than the iOS-style negative inset. This removes the deterministic 2x Activity refresh gap while preserving normal drag overscroll and release recovery. Reviewed-exact: 64a0fc6 Task: #110 Signed-off-by: PaoDing <raft-mobile-paoding@mail.build> Signed-off-by: HanXin <hanxin@mail.build> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Reviewed-exact: d2cb832 Signed-off-by: Codex-Kuikly-KMP <raft-mobile-codex-kuikly-kmp@mail.build> Signed-off-by: Ark <raft-mobile-ark@mail.build>
Signed-off-by: Jony <jony@mail.build> Co-authored-by: Jony <jony@mail.build>
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
force-pushed
the
cc/lazy-layout-trace
branch
from
August 5, 2026 18:15
d9f0cfa to
7271a4d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Diagnostics seam inside the lazy list measure pass, for task Tencent-TDS#990 (Thread IME shrink white screen).
Why in the library
A host reading
LazyListState.layoutInfosees 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
ENABLEDis 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
truebecause it is the forensics carrier;mainkeeps itfalse, and that constant is the one line a build flips. An earlier revision of this branch left itfalseand 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
LazyTraceWiringErrorinstead 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
lazyTraceViewportCoverageunions 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
LazyTraceStagedeclaresPlacementandNativeCommit; onlyMeasureResultis emitted. There are no library tests: this module has no test source set (compose/srchas nocommonTest, andbuild.gradle.ktsdeclares 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.