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
5 changes: 5 additions & 0 deletions .changeset/element-template-alog-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---

---

No package release is required because this change only adds Element Template diagnostic ALog output behind the existing debug logging path, without changing public APIs, package exports, runtime/native contracts, or default production behavior.
108 changes: 108 additions & 0 deletions packages/react/runtime/__test__/element-template/debug/alog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { beforeEach, describe, expect, it } from 'vitest';

import { BackgroundElementTemplateInstance } from '../../../src/element-template/background/instance.js';
import { backgroundElementTemplateInstanceManager } from '../../../src/element-template/background/manager.js';
import {
formatElementTemplateUpdateCommands,
printElementTemplateTreeToString,
} from '../../../src/element-template/debug/alog.js';
import { ElementTemplateUpdateOps } from '../../../src/element-template/protocol/opcodes.js';

describe('ElementTemplate alog helpers', () => {
beforeEach(() => {
backgroundElementTemplateInstanceManager.clear();
backgroundElementTemplateInstanceManager.nextId = 0;
});

it('formats update command streams into readable operations', () => {
expect(formatElementTemplateUpdateCommands([
ElementTemplateUpdateOps.createTemplate,
11,
'_et_card',
'main.js',
['title'],
[[12]],
ElementTemplateUpdateOps.setAttribute,
11,
0,
'updated',
ElementTemplateUpdateOps.insertNode,
11,
1,
12,
0,
ElementTemplateUpdateOps.removeNode,
11,
1,
12,
])).toEqual([
{
op: 'createTemplate',
handleId: 11,
templateKey: '_et_card',
bundleUrl: 'main.js',
attributeSlots: ['title'],
elementSlots: [[12]],
},
{
op: 'setAttribute',
targetId: 11,
attrSlotIndex: 0,
value: 'updated',
},
{
op: 'insertNode',
targetId: 11,
elementSlotIndex: 1,
childId: 12,
referenceId: 0,
},
{
op: 'removeNode',
targetId: 11,
elementSlotIndex: 1,
childId: 12,
},
]);
});

it('keeps unknown opcodes readable without throwing', () => {
expect(formatElementTemplateUpdateCommands([99, 'leftover'])).toEqual([
{
op: 'unknown',
opcode: 99,
index: 0,
remaining: ['leftover'],
},
]);
});

it('formats missing command streams as an empty list', () => {
expect(formatElementTemplateUpdateCommands(undefined)).toEqual([]);
});

it('prints a compact background tree summary', () => {
const root = new BackgroundElementTemplateInstance('root');
const card = new BackgroundElementTemplateInstance('_et_card', ['title']);
const text = new BackgroundElementTemplateInstance('__et_builtin_raw_text__', ['hello']);

root.appendChild(card);
card.appendChild(text);
card.elementSlots[0] = [text];
card.elementSlots[1] = [];

const output = printElementTemplateTreeToString(root);

expect(output).toContain('root#1');
expect(output).toContain('_et_card#2');
expect(output).toContain('attributeSlots: ["title"]');
expect(output).toContain('elementSlots[0]: [3]');
expect(output).not.toContain('elementSlots[1]');
expect(output).toContain('__et_builtin_raw_text__#3');
expect(output).toContain('attributeSlots: ["hello"]');
});

it('prints an empty marker for missing roots', () => {
expect(printElementTemplateTreeToString(null)).toBe('<empty>');
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { options } from 'preact';

import * as elementTemplateAlog from '../../../../src/element-template/debug/alog.js';
import {
installElementTemplateCommitHook,
markElementTemplateHydrated,
Expand Down Expand Up @@ -48,6 +49,7 @@ describe('ElementTemplate commit hook', () => {
});

afterEach(() => {
globalThis.__ALOG__ = true;
envManager.switchToMainThread();
lynx.getJSContext().removeEventListener(ElementTemplateLifecycleConstant.update, onUpdate);
envManager.switchToBackground();
Expand Down Expand Up @@ -117,6 +119,40 @@ describe('ElementTemplate commit hook', () => {
envManager.switchToBackground();
});

it('logs post-hydration update commits when alog is enabled', () => {
const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void };
alog.mockClear();

markElementTemplateHydrated();
GlobalCommitContext.ops = createRawTextOps(1, 'hello');
GlobalCommitContext.flushOptions = { nativeUpdateDataOrder: 7 };
GlobalCommitContext.flowIds = [101, 202];

options.__c?.({} as unknown as object, []);

const output = alog.mock.calls.map(args => String(args[0])).join('\n');
expect(output).toContain('[ReactLynxDebug] ElementTemplate BTS -> MTS update');
expect(output).toContain('createTemplate');
expect(output).toContain('nativeUpdateDataOrder');
expect(output).toContain('101');
});

it('does not format update commit alog when alog is disabled', () => {
globalThis.__ALOG__ = false;
const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void };
alog.mockClear();
const formatSpy = vi.spyOn(elementTemplateAlog, 'formatElementTemplateUpdateCommands');

markElementTemplateHydrated();
GlobalCommitContext.ops = createRawTextOps(1, 'hello');
GlobalCommitContext.flushOptions = { nativeUpdateDataOrder: 7 };

options.__c?.({} as unknown as object, []);

expect(formatSpy).not.toHaveBeenCalled();
expect(alog.mock.calls).toHaveLength(0);
});

it('is idempotent', () => {
installElementTemplateCommitHook();
installElementTemplateCommitHook();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import * as elementTemplateAlog from '../../../../src/element-template/debug/alog.js';
import {
installElementTemplateHydrationListener,
resetElementTemplateHydrationListener,
Expand Down Expand Up @@ -43,6 +44,7 @@ describe('ElementTemplate hydration listener', () => {
});

afterEach(() => {
globalThis.__ALOG__ = true;
resetElementTemplateHydrationListener();
});

Expand Down Expand Up @@ -169,6 +171,73 @@ describe('ElementTemplate hydration listener', () => {
]);
});

it('logs hydrate payload and background tree states when alog is enabled', () => {
envManager.switchToBackground();
installElementTemplateHydrationListener();

const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void };
alog.mockClear();

const backgroundRoot = __root as BackgroundElementTemplateInstance;
const after = new BackgroundElementTemplateInstance('_et_test', ['before']);
backgroundRoot.appendChild(after);

envManager.switchToMainThread();
lynx.getJSContext().dispatchEvent({
type: ElementTemplateLifecycleConstant.hydrate,
data: [
{
templateKey: '_et_test',
attributeSlots: ['after'],
elementSlots: [],
uid: -1,
} satisfies SerializedElementTemplate,
],
});

envManager.switchToBackground();

const output = alog.mock.calls.map(args => String(args[0])).join('\n');
expect(output).toContain('[ReactLynxDebug] ElementTemplate MTS -> BTS hydrate');
expect(output).toContain('BackgroundElementTemplate tree before hydration');
expect(output).toContain('BackgroundElementTemplate tree after hydration');
expect(output).toContain('setAttribute');
});

it('does not format hydrate alog when alog is disabled', () => {
globalThis.__ALOG__ = false;
envManager.switchToBackground();
installElementTemplateHydrationListener();

const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void };
alog.mockClear();
const formatSpy = vi.spyOn(elementTemplateAlog, 'formatElementTemplateUpdateCommands');
const printSpy = vi.spyOn(elementTemplateAlog, 'printElementTemplateTreeToString');

const backgroundRoot = __root as BackgroundElementTemplateInstance;
const after = new BackgroundElementTemplateInstance('_et_test', ['before']);
backgroundRoot.appendChild(after);

envManager.switchToMainThread();
lynx.getJSContext().dispatchEvent({
type: ElementTemplateLifecycleConstant.hydrate,
data: [
{
templateKey: '_et_test',
attributeSlots: ['after'],
elementSlots: [],
uid: -1,
} satisfies SerializedElementTemplate,
],
});

envManager.switchToBackground();

expect(formatSpy).not.toHaveBeenCalled();
expect(printSpy).not.toHaveBeenCalled();
expect(alog.mock.calls).toHaveLength(0);
});

it('reports illegal handleId 0 during hydrate', () => {
envManager.switchToBackground();
installElementTemplateHydrationListener();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import * as elementTemplateAlog from '../../../../src/element-template/debug/alog.js';
import {
installElementTemplatePatchListener,
resetElementTemplatePatchListener,
} from '../../../../src/element-template/native/patch-listener.js';
import { ElementTemplateLifecycleConstant } from '../../../../src/element-template/protocol/lifecycle-constant.js';
import { ElementTemplateUpdateOps } from '../../../../src/element-template/protocol/opcodes.js';
import { ElementTemplateEnvManager } from '../../test-utils/debug/envManager.js';
import { registerBuiltinRawTextTemplate } from '../../test-utils/debug/registry.js';

function createRawTextOps(id: number, text: string) {
return [
ElementTemplateUpdateOps.createTemplate,
id,
'__et_builtin_raw_text__',
null,
[text],
[],
];
}

describe('ElementTemplate patch listener alog', () => {
const envManager = new ElementTemplateEnvManager();

beforeEach(() => {
vi.clearAllMocks();
envManager.resetEnv('main');
registerBuiltinRawTextTemplate();
installElementTemplatePatchListener();
});

afterEach(() => {
globalThis.__ALOG__ = true;
resetElementTemplatePatchListener();
});

it('logs decoded update payloads before applying patches', () => {
const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void };
alog.mockClear();

envManager.switchToBackground(() => {
lynx.getCoreContext().dispatchEvent({
type: ElementTemplateLifecycleConstant.update,
data: {
ops: createRawTextOps(1, 'hello'),
flushOptions: { nativeUpdateDataOrder: 7 },
flowIds: [101, 202],
},
});
});
envManager.switchToMainThread();

const output = alog.mock.calls.map(args => String(args[0])).join('\n');
expect(output).toContain('[ReactLynxDebug] ElementTemplate main-thread patch');
expect(output).toContain('createTemplate');
expect(output).toContain('nativeUpdateDataOrder');
expect(output).toContain('101');
});

it('does not format patch alog when alog is disabled', () => {
globalThis.__ALOG__ = false;
const alog = console.alog as unknown as { mock: { calls: unknown[][] }; mockClear(): void };
alog.mockClear();
const formatSpy = vi.spyOn(elementTemplateAlog, 'formatElementTemplateUpdateCommands');

envManager.switchToBackground(() => {
lynx.getCoreContext().dispatchEvent({
type: ElementTemplateLifecycleConstant.update,
data: {
ops: createRawTextOps(1, 'hello'),
flushOptions: { nativeUpdateDataOrder: 7 },
},
});
});
envManager.switchToMainThread();

expect(formatSpy).not.toHaveBeenCalled();
expect(alog.mock.calls).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { options } from 'preact';
import { GlobalCommitContext, resetGlobalCommitContext } from './commit-context.js';
import { COMMIT } from '../../shared/render-constants.js';
import { hook } from '../../utils.js';
import { formatElementTemplateUpdateCommands } from '../debug/alog.js';
import { profileEnd, profileStart } from '../debug/profile.js';
import { globalPipelineOptions, markTiming, markTimingLegacy, setPipeline } from '../lynx/performance.js';
import { ElementTemplateLifecycleConstant } from '../protocol/lifecycle-constant.js';
Expand Down Expand Up @@ -53,6 +54,21 @@ export function installElementTemplateCommitHook(): void {
profileEnd();
}

if (typeof __ALOG__ !== 'undefined' && __ALOG__) {
console.alog?.(
'[ReactLynxDebug] ElementTemplate BTS -> MTS update:\n'
+ JSON.stringify(
{
ops: formatElementTemplateUpdateCommands(GlobalCommitContext.ops),
flushOptions: GlobalCommitContext.flushOptions,
flowIds: GlobalCommitContext.flowIds,
},
null,
2,
),
);
}

lynx.getCoreContext().dispatchEvent({
type: ElementTemplateLifecycleConstant.update,
data: {
Expand Down
Loading
Loading