diff --git a/packages/core/profiling/index.ts b/packages/core/profiling/index.ts index 3906e95889..d76dfc77f1 100644 --- a/packages/core/profiling/index.ts +++ b/packages/core/profiling/index.ts @@ -3,6 +3,19 @@ import appConfig from '~/package.json'; /* eslint-disable prefer-rest-params */ declare let __startCPUProfiler: any; declare let __stopCPUProfiler: any; +declare const __NS_PROFILING_DEBUG__: boolean | undefined; +declare const __NS_PROFILING_DEBUG_CONSOLE__: boolean | undefined; + +const enum MemberType { + Static, + Instance, +} + +export enum Level { + none, + lifecycle, + timeline, +} export function uptime(): number { return global.android ? (org).nativescript.Process.getUpTime() : global.__tns_uptime(); @@ -28,19 +41,92 @@ export interface TimerInfo { runCount: number; } -// Use object instead of map as it is a bit faster -const timers: { [index: string]: TimerInfo } = {}; +// Global singleton state (prevents duplication under multiple module instances) const anyGlobal = global; -const profileNames: string[] = []; +type ProfilingDebugPhase = 'timer-start' | 'timer-stop' | 'timer-stop-pending' | 'timer-reset' | 'wrap-instance' | 'wrap-static' | 'wrap-named' | 'wrap-function'; + +export interface ProfilingDebugEntry { + phase: ProfilingDebugPhase; + name: string; + timestamp: number; + detail?: { runCount?: number; count?: number; totalTime?: number; note?: string }; +} + +type ProfilerState = { + timers: { [index: string]: TimerInfo }; + profileNames: string[]; + tracingLevel: Level; + profileFunctionFactory?: (fn: F, name: string, type?: MemberType) => F; + debug?: boolean; + debugEvents?: ProfilingDebugEntry[]; + debugConsole?: boolean; +}; +const __nsProfiling: ProfilerState = (anyGlobal.__nsProfiling ||= { + timers: {}, + profileNames: [], + tracingLevel: undefined as any, + profileFunctionFactory: undefined, +}); +// Initialize default tracing level if first load +if (__nsProfiling.tracingLevel === undefined) { + __nsProfiling.tracingLevel = Level.none; +} +const debugEnabledFromDefine = typeof __NS_PROFILING_DEBUG__ !== 'undefined' ? __NS_PROFILING_DEBUG__ : undefined; +const debugConsoleFromDefine = typeof __NS_PROFILING_DEBUG_CONSOLE__ !== 'undefined' ? __NS_PROFILING_DEBUG_CONSOLE__ : undefined; +if (typeof __nsProfiling.debug !== 'boolean') { + __nsProfiling.debug = debugEnabledFromDefine ?? false; +} +if (!Array.isArray(__nsProfiling.debugEvents)) { + __nsProfiling.debugEvents = []; +} +if (typeof __nsProfiling.debugConsole !== 'boolean') { + __nsProfiling.debugConsole = debugConsoleFromDefine ?? true; +} +// Use object instead of map as it is a bit faster +const timers: { [index: string]: TimerInfo } = __nsProfiling.timers; +const profileNames: string[] = __nsProfiling.profileNames; +const debugEvents = __nsProfiling.debugEvents!; export const time = (global.__time || Date.now) as () => number; +function recordDebugEvent(phase: ProfilingDebugPhase, name: string, detail?: ProfilingDebugEntry['detail']): void { + if (!__nsProfiling.debug) { + return; + } + const entry: ProfilingDebugEntry = { + phase, + name, + timestamp: time(), + detail, + }; + debugEvents.push(entry); + if (__nsProfiling.debugConsole !== false) { + const summary = detail + ? Object.entries(detail) + .map(([key, value]) => `${key}=${value}`) + .join(' ') + : ''; + console.log(`[profiling:${phase}] ${name}${summary ? ' ' + summary : ''}`); + } +} + +function summarizeTimerInfo(info: TimerInfo | undefined) { + return info + ? { + runCount: info.runCount, + count: info.count, + totalTime: info.totalTime, + } + : undefined; +} + export function start(name: string): void { let info = timers[name]; if (info) { info.currentStart = time(); info.runCount++; + recordDebugEvent('timer-start', name, summarizeTimerInfo(info)); } else { info = { totalTime: 0, @@ -50,6 +136,7 @@ export function start(name: string): void { }; timers[name] = info; profileNames.push(name); + recordDebugEvent('timer-start', name, summarizeTimerInfo(info)); } } @@ -64,11 +151,13 @@ export function stop(name: string): TimerInfo { info.runCount--; if (info.runCount) { info.count++; + recordDebugEvent('timer-stop-pending', name, summarizeTimerInfo(info)); } else { info.lastTime = time() - info.currentStart; info.totalTime += info.lastTime; info.count++; info.currentStart = 0; + recordDebugEvent('timer-stop', name, summarizeTimerInfo(info)); } } else { throw new Error(`Timer ${name} paused more times than started.`); @@ -131,19 +220,9 @@ function timelineProfileFunctionFactory(fn: F, name: string, }; } -const enum MemberType { - Static, - Instance, -} - -export enum Level { - none, - lifecycle, - timeline, -} -let tracingLevel: Level = Level.none; +let tracingLevel: Level = __nsProfiling.tracingLevel; -let profileFunctionFactory: (fn: F, name: string, type?: MemberType) => F; +let profileFunctionFactory: (fn: F, name: string, type?: MemberType) => F = __nsProfiling.profileFunctionFactory; export function enable(mode: InstrumentationMode = 'counters') { profileFunctionFactory = mode && @@ -157,6 +236,10 @@ export function enable(mode: InstrumentationMode = 'counters') { lifecycle: Level.lifecycle, timeline: Level.timeline, }[mode] || Level.none; + + // persist to global singleton so other module instances share the same state + __nsProfiling.profileFunctionFactory = profileFunctionFactory; + __nsProfiling.tracingLevel = tracingLevel; } try { @@ -173,10 +256,24 @@ try { export function disable() { profileFunctionFactory = undefined; + __nsProfiling.profileFunctionFactory = undefined; } function profileFunction(fn: F, customName?: string): F { - return profileFunctionFactory(fn, customName || fn.name); + const name = customName || fn.name; + recordDebugEvent('wrap-function', name); + if (profileFunctionFactory) { + return profileFunctionFactory(fn, name); + } + // Lazy wrapper: if factory not available at decoration time, defer to runtime + return function () { + const fac = (anyGlobal.__nsProfiling && anyGlobal.__nsProfiling.profileFunctionFactory) || profileFunctionFactory; + if (fac) { + const wrapped = fac(fn, name); + return wrapped.apply(this, arguments); + } + return fn.apply(this, arguments); + }; } const profileMethodUnnamed = (target: Object, key: symbol | string, descriptor) => { @@ -194,8 +291,22 @@ const profileMethodUnnamed = (target: Object, key: symbol | string, descriptor) const name = className + key?.toString(); - //editing the descriptor/value parameter - descriptor.value = profileFunctionFactory(originalMethod, name, MemberType.Instance); + // editing the descriptor/value parameter + // Always install a wrapper that records timing regardless of current factory state to match webpack behavior. + // If a profiling factory is active use it; otherwise fallback to counters start/stop directly. + if (profileFunctionFactory) { + descriptor.value = profileFunctionFactory(originalMethod, name, MemberType.Instance); + } else { + descriptor.value = function () { + start(name); + try { + return originalMethod.apply(this, arguments); + } finally { + stop(name); + } + }; + } + recordDebugEvent('wrap-instance', name); // return edited descriptor as opposed to overwriting the descriptor return descriptor; @@ -215,8 +326,20 @@ const profileStaticMethodUnnamed = (ctor: F, key: symbol | s } const name = className + key?.toString(); - //editing the descriptor/value parameter - descriptor.value = profileFunctionFactory(originalMethod, name, MemberType.Static); + // editing the descriptor/value parameter + if (profileFunctionFactory) { + descriptor.value = profileFunctionFactory(originalMethod, name, MemberType.Static); + } else { + descriptor.value = function () { + start(name); + try { + return originalMethod.apply(this, arguments); + } finally { + stop(name); + } + }; + } + recordDebugEvent('wrap-static', name); // return edited descriptor as opposed to overwriting the descriptor return descriptor; @@ -231,10 +354,22 @@ function profileMethodNamed(name: string): MethodDecorator { } const originalMethod = descriptor.value; - //editing the descriptor/value parameter - descriptor.value = profileFunctionFactory(originalMethod, name); + // editing the descriptor/value parameter + if (profileFunctionFactory) { + descriptor.value = profileFunctionFactory(originalMethod, name); + } else { + descriptor.value = function () { + start(name); + try { + return originalMethod.apply(this, arguments); + } finally { + stop(name); + } + }; + } // return edited descriptor as opposed to overwriting the descriptor + recordDebugEvent('wrap-named', name); return descriptor; }; } @@ -245,40 +380,16 @@ const voidMethodDecorator = () => { export function profile(nameFnOrTarget?: string | Function | Object, fnOrKey?: Function | string | symbol, descriptor?: PropertyDescriptor, attrs?: any): any { if (typeof nameFnOrTarget === 'object' && (typeof fnOrKey === 'string' || typeof fnOrKey === 'symbol')) { - if (!profileFunctionFactory) { - return; - } - return profileMethodUnnamed(nameFnOrTarget, fnOrKey, descriptor); } else if (typeof nameFnOrTarget === 'function' && (typeof fnOrKey === 'string' || typeof fnOrKey === 'symbol')) { - if (!profileFunctionFactory) { - return; - } - return profileStaticMethodUnnamed(nameFnOrTarget, fnOrKey, descriptor); } else if (typeof nameFnOrTarget === 'string' && typeof fnOrKey === 'function') { - if (!profileFunctionFactory) { - return fnOrKey; - } - return profileFunction(fnOrKey, nameFnOrTarget); } else if (typeof nameFnOrTarget === 'function') { - if (!profileFunctionFactory) { - return nameFnOrTarget; - } - return profileFunction(nameFnOrTarget); } else if (typeof nameFnOrTarget === 'string') { - if (!profileFunctionFactory) { - return voidMethodDecorator; - } - return profileMethodNamed(nameFnOrTarget); } else { - if (!profileFunctionFactory) { - return voidMethodDecorator; - } - return profileMethodUnnamed; } } @@ -300,8 +411,10 @@ export function resetProfiles(): void { if (info) { if (info.runCount) { console.log('---- timer with name [' + name + "] is currently running and won't be reset"); + recordDebugEvent('timer-reset', name, summarizeTimerInfo(info)); } else { timers[name] = undefined; + recordDebugEvent('timer-reset', name, summarizeTimerInfo(info)); } } }); diff --git a/packages/core/ui/core/view/index.ios.ts b/packages/core/ui/core/view/index.ios.ts index deff407d4e..1de59f7e54 100644 --- a/packages/core/ui/core/view/index.ios.ts +++ b/packages/core/ui/core/view/index.ios.ts @@ -77,6 +77,12 @@ export class View extends ViewCommon { public requestLayout(): void { this._privateFlags |= PFLAG_FORCE_LAYOUT; + if (global.__nsProfiling?.debug) { + console.log('[layout-debug:request-layout]', { + view: `${this}`, + parent: `${this.parent}`, + }); + } super.requestLayout(); const nativeView = this.nativeViewProtected; diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index ee5dff2014..e269ad763c 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -1072,6 +1072,12 @@ export abstract class ViewCommon extends ViewBase { public requestLayout(): void { this._isLayoutValid = false; + if ((global).__nsProfiling?.debug) { + console.log('[layout-debug:invalidate]', { + view: `${this}`, + isLayoutValid: this._isLayoutValid, + }); + } super.requestLayout(); } diff --git a/packages/core/ui/core/view/view-helper/view-helper-common.ts b/packages/core/ui/core/view/view-helper/view-helper-common.ts index 90863943fd..426d921d58 100644 --- a/packages/core/ui/core/view/view-helper/view-helper-common.ts +++ b/packages/core/ui/core/view/view-helper/view-helper-common.ts @@ -3,6 +3,14 @@ import { View as ViewDefinition } from '..'; import { CoreTypes } from '../../../../core-types'; import { layout, Trace } from './view-helper-shared'; +const globalAny: any = global; +function debugLayoutChild(tag: string, payload: Record): void { + if (!globalAny.__nsProfiling?.debug) { + return; + } + console.log(`[layout-child-debug:${tag}]`, payload); +} + export class ViewHelper { public static measureChild(parent: ViewDefinition, child: ViewDefinition, widthMeasureSpec: number, heightMeasureSpec: number): { measuredWidth: number; measuredHeight: number } { let measureWidth = 0; @@ -125,6 +133,19 @@ export class ViewHelper { const childBottom = Math.round(childTop + childHeight); childLeft = Math.round(childLeft); childTop = Math.round(childTop); + debugLayoutChild('computed', { + parent: `${child.parent}`, + child: `${child}`, + hAlignment, + vAlignment, + left, + right, + childMeasuredWidth: child.getMeasuredWidth(), + effectiveMarginLeft, + effectiveMarginRight, + computedLeft: childLeft, + computedRight: childRight, + }); if (Trace.isEnabled()) { Trace.write(child.parent + ' :layoutChild: ' + child + ' ' + childLeft + ', ' + childTop + ', ' + childRight + ', ' + childBottom, Trace.categories.Layout); diff --git a/packages/core/ui/layouts/dock-layout/index.ios.ts b/packages/core/ui/layouts/dock-layout/index.ios.ts index 43aafeca63..7f9ff05c86 100644 --- a/packages/core/ui/layouts/dock-layout/index.ios.ts +++ b/packages/core/ui/layouts/dock-layout/index.ios.ts @@ -95,10 +95,14 @@ export class DockLayout extends DockLayoutBase { this.eachLayoutChild((child, last) => { let childWidth = child.getMeasuredWidth() + child.effectiveMarginLeft + child.effectiveMarginRight; let childHeight = child.getMeasuredHeight() + child.effectiveMarginTop + child.effectiveMarginBottom; + const availableWidth = remainingWidth; + const availableHeight = remainingHeight; + const horizontalAlignment = child.horizontalAlignment; + const extendForAlignment = horizontalAlignment === 'center' || horizontalAlignment === 'right' || horizontalAlignment === 'end'; if (last && this.stretchLastChild) { // Last child with stretch - give it all the space and return; - View.layoutChild(this, child, x, y, x + remainingWidth, y + remainingHeight); + View.layoutChild(this, child, x, y, x + availableWidth, y + availableHeight); return; } @@ -108,23 +112,28 @@ export class DockLayout extends DockLayoutBase { case 'top': childLeft = x; childTop = y; - childWidth = remainingWidth; + childWidth = availableWidth; y += childHeight; - remainingHeight = Math.max(0, remainingHeight - childHeight); + remainingHeight = Math.max(0, availableHeight - childHeight); break; case 'bottom': childLeft = x; childTop = y + remainingHeight - childHeight; - childWidth = remainingWidth; - remainingHeight = Math.max(0, remainingHeight - childHeight); + childWidth = availableWidth; + remainingHeight = Math.max(0, availableHeight - childHeight); break; case 'right': childLeft = x + remainingWidth - childWidth; childTop = y; childHeight = remainingHeight; - remainingWidth = Math.max(0, remainingWidth - childWidth); + remainingWidth = Math.max(0, availableWidth - childWidth); + if (extendForAlignment && availableWidth > childWidth) { + const slotLeft = childLeft - (availableWidth - childWidth); + View.layoutChild(this, child, slotLeft, childTop, slotLeft + availableWidth, childTop + childHeight); + return; + } break; case 'left': @@ -133,7 +142,11 @@ export class DockLayout extends DockLayoutBase { childTop = y; childHeight = remainingHeight; x += childWidth; - remainingWidth = Math.max(0, remainingWidth - childWidth); + remainingWidth = Math.max(0, availableWidth - childWidth); + if (extendForAlignment && availableWidth > childWidth) { + View.layoutChild(this, child, childLeft, childTop, childLeft + availableWidth, childTop + childHeight); + return; + } break; }