From f3ec20381df84c8ea8db61ec3c7b411439e4a9b8 Mon Sep 17 00:00:00 2001 From: yradex <11014207+Yradex@users.noreply.github.com> Date: Thu, 14 May 2026 15:06:40 +0800 Subject: [PATCH 1/2] feat(react): add element template alog diagnostics --- .../debug/elementPAPICall.test.ts | 122 ++++++++++++++++++ .../element-template/native/index.test.ts | 9 ++ .../runtime/prop-adapters/event.test.ts | 40 +++++- .../element-template/debug/elementPAPICall.ts | 81 ++++++++++++ .../src/element-template/native/index.ts | 5 + .../element-template/prop-adapters/event.ts | 16 +++ 6 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 packages/react/runtime/__test__/element-template/debug/elementPAPICall.test.ts create mode 100644 packages/react/runtime/src/element-template/debug/elementPAPICall.ts diff --git a/packages/react/runtime/__test__/element-template/debug/elementPAPICall.test.ts b/packages/react/runtime/__test__/element-template/debug/elementPAPICall.test.ts new file mode 100644 index 0000000000..351e8effb8 --- /dev/null +++ b/packages/react/runtime/__test__/element-template/debug/elementPAPICall.test.ts @@ -0,0 +1,122 @@ +// Copyright 2026 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { initElementTemplatePAPICallAlog } from '../../../src/element-template/debug/elementPAPICall.js'; + +describe('ElementTemplate PAPI alog wrapper', () => { + const originalProfile = globalThis.__PROFILE__; + + beforeEach(() => { + vi.clearAllMocks(); + globalThis.__PROFILE__ = true; + }); + + afterEach(() => { + globalThis.__PROFILE__ = originalProfile; + }); + + it('wraps ET PAPI calls and formats native refs', () => { + const templateRef = { id: 1 }; + const childRef = { id: 2 }; + const circular: Record = {}; + circular['self'] = circular; + const jsonUndefined = { toJSON: () => undefined }; + function namedHandler() {} + const anonymousHandler = () => {}; + Object.defineProperty(anonymousHandler, 'name', { value: '' }); + + const target = { + __CreateElementTemplate: vi.fn(() => templateRef), + __SetAttributeOfElementTemplate: vi.fn(), + __InsertNodeToElementTemplate: vi.fn(() => childRef), + __RemoveNodeFromElementTemplate: vi.fn(() => null), + __SerializeElementTemplate: vi.fn(() => Symbol.for('serialized')), + } satisfies Record; + + initElementTemplatePAPICallAlog(target); + + expect((target.__CreateElementTemplate as (...args: unknown[]) => unknown)( + '_et_card', + null, + ['title'], + [], + 17, + )).toBe(templateRef); + (target.__SetAttributeOfElementTemplate as (...args: unknown[]) => unknown)( + templateRef, + 0, + [ + templateRef, + undefined, + null, + namedHandler, + anonymousHandler, + Symbol.for('slot'), + circular, + jsonUndefined, + ], + null, + ); + expect((target.__InsertNodeToElementTemplate as (...args: unknown[]) => unknown)( + templateRef, + 1, + childRef, + undefined, + )).toBe(childRef); + expect((target.__RemoveNodeFromElementTemplate as (...args: unknown[]) => unknown)( + templateRef, + 1, + childRef, + )).toBeNull(); + expect((target.__SerializeElementTemplate as (...args: unknown[]) => unknown)(templateRef)).toBe( + Symbol.for('serialized'), + ); + + const logs = (console.alog as unknown as { mock: { calls: unknown[][] } }).mock.calls + .map(args => String(args[0])) + .join('\n'); + expect(logs).toContain( + '__CreateElementTemplate("_et_card", null, ["title"], [], 17) => _et_card#17', + ); + expect(logs).toContain( + '__SetAttributeOfElementTemplate(_et_card#17, 0, [_et_card#17, undefined, null, [Function namedHandler], [Function], Symbol(slot), [object Object], [object Object]], null)', + ); + expect(logs).toContain( + '__InsertNodeToElementTemplate(_et_card#17, 1, {"id":2}, undefined) => {"id":2}', + ); + expect(logs).toContain('__RemoveNodeFromElementTemplate(_et_card#17, 1, {"id":2})'); + expect(logs).toContain('__SerializeElementTemplate(_et_card#17) => Symbol(serialized)'); + expect(globalThis.lynx.performance.profileStart).toHaveBeenCalledTimes(5); + expect(globalThis.lynx.performance.profileEnd).toHaveBeenCalledTimes(5); + }); + + it('skips missing APIs and keeps logging optional', () => { + const originalAlog = console.alog; + const createElementTemplate = vi.fn(() => null); + const target = { + __CreateElementTemplate: createElementTemplate, + } satisfies Record; + + globalThis.__PROFILE__ = false; + console.alog = undefined; + try { + initElementTemplatePAPICallAlog(target); + + expect((target.__CreateElementTemplate as (...args: unknown[]) => unknown)( + '_et_empty', + null, + [], + [], + 3, + )).toBeNull(); + expect(createElementTemplate).toHaveBeenCalledTimes(1); + expect(globalThis.lynx.performance.profileStart).not.toHaveBeenCalled(); + expect(globalThis.lynx.performance.profileEnd).not.toHaveBeenCalled(); + } finally { + console.alog = originalAlog; + } + }); +}); diff --git a/packages/react/runtime/__test__/element-template/native/index.test.ts b/packages/react/runtime/__test__/element-template/native/index.test.ts index ab4651b096..201bb44eb6 100644 --- a/packages/react/runtime/__test__/element-template/native/index.test.ts +++ b/packages/react/runtime/__test__/element-template/native/index.test.ts @@ -10,10 +10,12 @@ describe('element-template native index wiring', () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); + globalThis.__ALOG_ELEMENT_API__ = undefined; }); afterEach(() => { process.env['NODE_ENV'] = originalNodeEnv; + globalThis.__ALOG_ELEMENT_API__ = undefined; vi.resetModules(); vi.doUnmock('../../../src/element-template/native/main-thread-api.js'); vi.doUnmock('../../../src/element-template/native/patch-listener.js'); @@ -24,6 +26,7 @@ describe('element-template native index wiring', () => { vi.doUnmock('../../../src/element-template/background/hydration-listener.js'); vi.doUnmock('../../../src/element-template/background/commit-hook.js'); vi.doUnmock('../../../src/element-template/background/instance.js'); + vi.doUnmock('../../../src/element-template/debug/elementPAPICall.js'); vi.doUnmock('../../../src/element-template/debug/profile.js'); vi.doUnmock('../../../src/element-template/lynx/env.js'); vi.doUnmock('../../../src/element-template/lynx/performance.js'); @@ -32,10 +35,12 @@ describe('element-template native index wiring', () => { it('installs main-thread wiring only on main thread', async () => { envManager.resetEnv('main'); + globalThis.__ALOG_ELEMENT_API__ = true; const injectCalledByNative = vi.fn(); const installElementTemplatePatchListener = vi.fn(); const installOnMtsDestruction = vi.fn(); + const initElementTemplatePAPICallAlog = vi.fn(); const initProfileHook = vi.fn(); const setupLynxEnv = vi.fn(); const installElementTemplateCommitHook = vi.fn(); @@ -53,6 +58,9 @@ describe('element-template native index wiring', () => { vi.doMock('../../../src/element-template/native/mts-destroy.js', () => ({ installOnMtsDestruction, })); + vi.doMock('../../../src/element-template/debug/elementPAPICall.js', () => ({ + initElementTemplatePAPICallAlog, + })); vi.doMock('../../../src/element-template/debug/profile.js', () => ({ initProfileHook, })); @@ -80,6 +88,7 @@ describe('element-template native index wiring', () => { await import('../../../src/element-template/native/index.js'); + expect(initElementTemplatePAPICallAlog).toHaveBeenCalledTimes(1); expect(injectCalledByNative).toHaveBeenCalledTimes(1); expect(installElementTemplatePatchListener).toHaveBeenCalledTimes(1); expect(installOnMtsDestruction).toHaveBeenCalledTimes(1); diff --git a/packages/react/runtime/__test__/element-template/runtime/prop-adapters/event.test.ts b/packages/react/runtime/__test__/element-template/runtime/prop-adapters/event.test.ts index ee86206ef7..09ef64943b 100644 --- a/packages/react/runtime/__test__/element-template/runtime/prop-adapters/event.test.ts +++ b/packages/react/runtime/__test__/element-template/runtime/prop-adapters/event.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { destroyElementTemplateBackgroundRuntime } from '../../../../src/element-template/background/destroy.js'; import { BackgroundElementTemplateInstance } from '../../../../src/element-template/background/instance.js'; @@ -31,6 +31,11 @@ describe('ElementTemplate event bridge', () => { backgroundElementTemplateInstanceManager.nextId = 0; clearEtAttrPlanMap(); clearEventState(); + globalThis.__ALOG__ = true; + }); + + afterEach(() => { + globalThis.__ALOG__ = true; }); it('dispatches publishEvent to the current handler', () => { @@ -43,6 +48,39 @@ describe('ElementTemplate event bridge', () => { expect(handler).toHaveBeenCalledWith(eventData); }); + it('logs dispatch metadata when alog is enabled', () => { + const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void }; + alog.mockClear(); + const received: unknown[] = []; + function onTap(data: unknown) { + received.push(data); + } + createEventInstance(-8, onTap); + + publishEvent('-8:0:', { type: 'tap', detail: { x: 1 } }); + + const output = alog.mock.calls.map(args => String(args[0])).join('\n'); + expect(received).toEqual([{ type: 'tap', detail: { x: 1 } }]); + expect(output).toContain('[ReactLynxDebug] ElementTemplate BTS received event'); + expect(output).toContain('"eventValue": "-8:0:"'); + expect(output).toContain('"type": "tap"'); + expect(output).toContain('"jsFunctionName": "onTap"'); + expect(output).toContain('"hasHandler": true'); + }); + + it('skips dispatch alog when alog is disabled', () => { + globalThis.__ALOG__ = false; + const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void }; + alog.mockClear(); + const handler = vi.fn(); + createEventInstance(-9, handler); + + publishEvent('-9:0:', { type: 'tap' }); + + expect(handler).toHaveBeenCalledWith({ type: 'tap' }); + expect(alog.mock.calls).toHaveLength(0); + }); + it('dispatches publicComponentEvent through the same handler lookup', () => { const handler = vi.fn(); const eventData = { type: 'tap' }; diff --git a/packages/react/runtime/src/element-template/debug/elementPAPICall.ts b/packages/react/runtime/src/element-template/debug/elementPAPICall.ts new file mode 100644 index 0000000000..ed663cc236 --- /dev/null +++ b/packages/react/runtime/src/element-template/debug/elementPAPICall.ts @@ -0,0 +1,81 @@ +// Copyright 2025 The Lynx Authors. All rights reserved. +// Licensed under the Apache License Version 2.0 that can be found in the +// LICENSE file in the root directory of this source tree. + +import { profileEnd, profileStart } from '../../shared/profile.js'; + +const elementTemplatePAPINameList = [ + '__CreateElementTemplate', + '__SetAttributeOfElementTemplate', + '__InsertNodeToElementTemplate', + '__RemoveNodeFromElementTemplate', + '__SerializeElementTemplate', +] as const; + +export function initElementTemplatePAPICallAlog(globalWithIndex: Record = globalThis): void { + let count = 0; + const elementTemplateMap = new Map(); + + for (const elementTemplatePAPIName of elementTemplatePAPINameList) { + const oldElementTemplatePAPI = globalWithIndex[elementTemplatePAPIName]; + if (typeof oldElementTemplatePAPI !== 'function') { + continue; + } + const callElementTemplatePAPI = oldElementTemplatePAPI as (...args: unknown[]) => unknown; + + globalWithIndex[elementTemplatePAPIName] = (...args: unknown[]): unknown => { + if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) { + profileStart(`ElementTemplatePAPI: ${elementTemplatePAPIName}`, { + args: { + args: formatValue(args, elementTemplateMap), + }, + }); + } + + const result = callElementTemplatePAPI(...args); + + if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) { + profileEnd(); + } + + if (elementTemplatePAPIName === '__CreateElementTemplate' && result != null) { + elementTemplateMap.set(result, `${String(args[0])}#${String(args[4])}`); + } + + const formattedResult = result == null ? undefined : formatValue(result, elementTemplateMap); + console.alog?.( + `[ReactLynxDebug] ElementTemplate API call #${++count}: ${elementTemplatePAPIName}(${ + args.map(arg => formatValue(arg, elementTemplateMap)).join(', ') + })${formattedResult == null ? '' : ` => ${formattedResult}`}`, + ); + return result; + }; + } +} + +function formatValue(value: unknown, elementTemplateMap: Map): string { + if (elementTemplateMap.has(value)) { + return elementTemplateMap.get(value)!; + } + if (value === undefined) { + return 'undefined'; + } + if (value === null) { + return 'null'; + } + if (Array.isArray(value)) { + return '[' + value.map(item => formatValue(item, elementTemplateMap)).join(', ') + ']'; + } + if (typeof value === 'function') { + return `[Function${value.name ? ` ${value.name}` : ''}]`; + } + if (typeof value === 'symbol') { + return value.toString(); + } + try { + const formatted = JSON.stringify(value); + return formatted ?? Object.prototype.toString.call(value); + } catch { + return Object.prototype.toString.call(value); + } +} diff --git a/packages/react/runtime/src/element-template/native/index.ts b/packages/react/runtime/src/element-template/native/index.ts index 8fc4b2a3b3..b1f21b671f 100644 --- a/packages/react/runtime/src/element-template/native/index.ts +++ b/packages/react/runtime/src/element-template/native/index.ts @@ -11,6 +11,7 @@ import { installElementTemplateCommitHook } from '../background/commit-hook.js'; import { setupBackgroundElementTemplateDocument } from '../background/document.js'; import { installElementTemplateHydrationListener } from '../background/hydration-listener.js'; import { BackgroundElementTemplateInstance } from '../background/instance.js'; +import { initElementTemplatePAPICallAlog } from '../debug/elementPAPICall.js'; import { initProfileHook } from '../debug/profile.js'; import { setupLynxEnv } from '../lynx/env.js'; import { initTimingAPI } from '../lynx/performance.js'; @@ -18,6 +19,10 @@ import { publicComponentEvent, publishEvent, resetEventStateForRuntime } from '. import { setRoot } from '../runtime/page/root-instance.js'; function init(): void { + if (typeof __ALOG_ELEMENT_API__ !== 'undefined' && __ALOG_ELEMENT_API__) { + initElementTemplatePAPICallAlog(); + } + if (__MAIN_THREAD__) { installMainThreadHooks(); injectCalledByNative(); diff --git a/packages/react/runtime/src/element-template/prop-adapters/event.ts b/packages/react/runtime/src/element-template/prop-adapters/event.ts index 129a50ba2a..fa4e4e3cb9 100644 --- a/packages/react/runtime/src/element-template/prop-adapters/event.ts +++ b/packages/react/runtime/src/element-template/prop-adapters/event.ts @@ -11,6 +11,22 @@ let queuePendingEvents = false; function dispatchEvent(eventValue: string, data: unknown): boolean { const handler = backgroundElementTemplateInstanceManager.getRawAttributeValueByEventValue(eventValue); + if (typeof __ALOG__ !== 'undefined' && __ALOG__) { + console.alog?.( + `[ReactLynxDebug] ElementTemplate BTS received event:\n${ + JSON.stringify( + { + eventValue, + type: (data as { type?: unknown } | null)?.type, + jsFunctionName: typeof handler === 'function' ? handler.name : '', + hasHandler: typeof handler === 'function', + }, + null, + 2, + ) + }`, + ); + } if (typeof handler !== 'function') { return false; } From 64a00d973a4a3a8cc2b537e02a1b6e2a151c845f Mon Sep 17 00:00:00 2001 From: yradex <11014207+Yradex@users.noreply.github.com> Date: Thu, 14 May 2026 16:08:45 +0800 Subject: [PATCH 2/2] fix(react): harden element template alog diagnostics --- .../debug/elementPAPICall.test.ts | 23 +++++++++++++++++++ .../element-template/debug/elementPAPICall.ts | 14 +++++++---- .../element-template/prop-adapters/event.ts | 12 +++++----- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/react/runtime/__test__/element-template/debug/elementPAPICall.test.ts b/packages/react/runtime/__test__/element-template/debug/elementPAPICall.test.ts index 351e8effb8..0fc9be43a8 100644 --- a/packages/react/runtime/__test__/element-template/debug/elementPAPICall.test.ts +++ b/packages/react/runtime/__test__/element-template/debug/elementPAPICall.test.ts @@ -119,4 +119,27 @@ describe('ElementTemplate PAPI alog wrapper', () => { console.alog = originalAlog; } }); + + it('ends the profile when a wrapped ET PAPI throws', () => { + const error = new Error('native failed'); + const target = { + __CreateElementTemplate: vi.fn(() => { + throw error; + }), + } satisfies Record; + + initElementTemplatePAPICallAlog(target); + + expect(() => + (target.__CreateElementTemplate as (...args: unknown[]) => unknown)( + '_et_error', + null, + [], + [], + 5, + ) + ).toThrow(error); + expect(globalThis.lynx.performance.profileStart).toHaveBeenCalledTimes(1); + expect(globalThis.lynx.performance.profileEnd).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/react/runtime/src/element-template/debug/elementPAPICall.ts b/packages/react/runtime/src/element-template/debug/elementPAPICall.ts index ed663cc236..5024d27d27 100644 --- a/packages/react/runtime/src/element-template/debug/elementPAPICall.ts +++ b/packages/react/runtime/src/element-template/debug/elementPAPICall.ts @@ -24,7 +24,8 @@ export function initElementTemplatePAPICallAlog(globalWithIndex: Record unknown; globalWithIndex[elementTemplatePAPIName] = (...args: unknown[]): unknown => { - if (typeof __PROFILE__ !== 'undefined' && __PROFILE__) { + const shouldProfile = typeof __PROFILE__ !== 'undefined' && __PROFILE__; + if (shouldProfile) { profileStart(`ElementTemplatePAPI: ${elementTemplatePAPIName}`, { args: { args: formatValue(args, elementTemplateMap), @@ -32,10 +33,13 @@ export function initElementTemplatePAPICallAlog(globalWithIndex: Record unknown; +export type EtEventHandler = (data: EventDataType) => unknown; -const pendingEvents: Array<[eventValue: string, data: unknown]> = []; +const pendingEvents: Array<[eventValue: string, data: EventDataType]> = []; let queuePendingEvents = false; -function dispatchEvent(eventValue: string, data: unknown): boolean { +function dispatchEvent(eventValue: string, data: EventDataType): boolean { const handler = backgroundElementTemplateInstanceManager.getRawAttributeValueByEventValue(eventValue); if (typeof __ALOG__ !== 'undefined' && __ALOG__) { console.alog?.( @@ -17,7 +17,7 @@ function dispatchEvent(eventValue: string, data: unknown): boolean { JSON.stringify( { eventValue, - type: (data as { type?: unknown } | null)?.type, + type: data.type, jsFunctionName: typeof handler === 'function' ? handler.name : '', hasHandler: typeof handler === 'function', }, @@ -54,7 +54,7 @@ export function getEventHandlerForEventValue(eventValue: string): EtEventHandler return typeof handler === 'function' ? (handler as EtEventHandler) : undefined; } -export function publishEvent(eventValue: string, data: unknown): void { +export function publishEvent(eventValue: string, data: EventDataType): void { if (dispatchEvent(eventValue, data)) { return; } @@ -66,7 +66,7 @@ export function publishEvent(eventValue: string, data: unknown): void { export function publicComponentEvent( _componentId: string, eventValue: string, - data: unknown, + data: EventDataType, ): void { publishEvent(eventValue, data); }