From 81a11f7e2230d1d800dd8c4eb71007fc10edbc2c Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Sun, 25 May 2025 22:21:46 +0200 Subject: [PATCH 01/13] feat: add stats tracking for components, elements, event listeners, timeouts, and intervals --- src/component.js | 4 + src/component/base/methods.js | 2 + src/component/base/timeouts_intervals.js | 8 ++ src/engines/L3/element.js | 7 ++ src/launch.js | 5 ++ src/lib/eventListeners.js | 8 ++ src/lib/stats.js | 109 +++++++++++++++++++++++ 7 files changed, 143 insertions(+) create mode 100644 src/lib/stats.js diff --git a/src/component.js b/src/component.js index 4e468953..9b988a11 100644 --- a/src/component.js +++ b/src/component.js @@ -24,6 +24,7 @@ import { reactive, getRaw } from './lib/reactivity/reactive.js' import { effect } from './lib/reactivity/effect.js' import Lifecycle from './lib/lifecycle.js' import symbols from './lib/symbols.js' +import { increment } from './lib/stats.js' import { stage, renderer } from './launch.js' @@ -212,6 +213,9 @@ const Component = (name = required('name'), config = required('config')) => { // finaly set the lifecycle state to ready (in the next tick) setTimeout(() => (this.lifecycle.state = 'ready')) + // Example: Increment component creation + increment('components', 'created') + // and return this return this } diff --git a/src/component/base/methods.js b/src/component/base/methods.js index 37593fa4..8b2a60c3 100644 --- a/src/component/base/methods.js +++ b/src/component/base/methods.js @@ -21,6 +21,7 @@ import eventListeners from '../../lib/eventListeners.js' import { trigger } from '../../lib/reactivity/effect.js' import { Log } from '../../lib/log.js' import { removeGlobalEffects } from '../../lib/reactivity/effect.js' +import { decrement } from '../../lib/stats.js' export default { focus: { @@ -87,6 +88,7 @@ export default { delete this[symbols.holder] Log.debug(`Destroyed component ${this.componentId}`) + decrement('components', 'deleted') }, writable: false, enumerable: true, diff --git a/src/component/base/timeouts_intervals.js b/src/component/base/timeouts_intervals.js index 5c667096..c891f3b9 100644 --- a/src/component/base/timeouts_intervals.js +++ b/src/component/base/timeouts_intervals.js @@ -16,6 +16,7 @@ */ import symbols from '../../lib/symbols.js' +import { increment, decrement } from '../../lib/stats.js' export default { $setTimeout: { @@ -23,12 +24,14 @@ export default { const timeoutId = setTimeout( () => { this[symbols.timeouts] = this[symbols.timeouts].filter((id) => id !== timeoutId) + decrement('timeouts', 'deleted') fn.apply(null, params) }, ms, params ) this[symbols.timeouts].push(timeoutId) + increment('timeouts', 'created') return timeoutId }, writable: false, @@ -40,6 +43,7 @@ export default { if (this[symbols.timeouts].indexOf(timeoutId) > -1) { this[symbols.timeouts] = this[symbols.timeouts].filter((id) => id !== timeoutId) clearTimeout(timeoutId) + decrement('timeouts', 'deleted') } }, writable: false, @@ -50,6 +54,7 @@ export default { value: function () { for (let i = 0; i < this[symbols.timeouts].length; i++) { clearTimeout(this[symbols.timeouts][i]) + decrement('timeouts', 'deleted') } this[symbols.timeouts] = [] }, @@ -61,6 +66,7 @@ export default { value: function (fn, ms, ...params) { const intervalId = setInterval(() => fn.apply(null, params), ms, params) this[symbols.intervals].push(intervalId) + increment('intervals', 'created') return intervalId }, writable: false, @@ -72,6 +78,7 @@ export default { if (this[symbols.intervals].indexOf(intervalId) > -1) { this[symbols.intervals] = this[symbols.intervals].filter((id) => id !== intervalId) clearInterval(intervalId) + decrement('intervals', 'deleted') } }, writable: false, @@ -82,6 +89,7 @@ export default { value: function () { for (let i = 0; i < this[symbols.intervals].length; i++) { clearInterval(this[symbols.intervals][i]) + decrement('intervals', 'active') } this[symbols.intervals] = [] }, diff --git a/src/engines/L3/element.js b/src/engines/L3/element.js index d254d657..dea44701 100644 --- a/src/engines/L3/element.js +++ b/src/engines/L3/element.js @@ -17,6 +17,7 @@ import { renderer } from './launch.js' import colors from '../../lib/colors/colors.js' +import { increment, decrement } from '../../lib/stats.js' import { Log } from '../../lib/log.js' import symbols from '../../lib/symbols.js' @@ -505,6 +506,9 @@ const Element = { this.config.parent.triggerLayout(this.config.parent.props) }) } + + // Increment element creation + increment('elements', 'created') }, set(prop, value) { if (value === undefined) return @@ -637,6 +641,9 @@ const Element = { // remove node reference this.node = null + + // Decrement element deletion + decrement('elements', 'deleted') }, get nodeId() { return this.node && this.node.id diff --git a/src/launch.js b/src/launch.js index 26936baf..f0e48c59 100644 --- a/src/launch.js +++ b/src/launch.js @@ -18,6 +18,7 @@ import Settings from './settings.js' import { initLog, Log } from './lib/log.js' import engine from './engine.js' +import { startLogging } from './lib/stats.js' import blitsPackageInfo from '../package.json' assert { type: 'json' } export let renderer = {} @@ -42,6 +43,10 @@ export default (App, target, settings) => { initLog() + if (settings.enableStatsLogger) { + startLogging() + } + rendererVersion().then((v) => { Log.info('Blits Version ', blitsPackageInfo.version) Log.info('Renderer Version ', v) diff --git a/src/lib/eventListeners.js b/src/lib/eventListeners.js index 6b96f6ff..93d91b2a 100644 --- a/src/lib/eventListeners.js +++ b/src/lib/eventListeners.js @@ -15,6 +15,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { increment, decrement } from './stats' + const eventsMap = new Map() const callbackCache = new Map() @@ -34,6 +36,9 @@ export default { components.add({ cb, priority }) callbackCache.delete(event) // Invalidate the callbackCache when a new callback is added + + // Log the event listener creation + increment('eventListeners', 'created') }, deregisterListener(component, event) { @@ -46,6 +51,9 @@ export default { componentsMap.delete(component) eventsMap.set(event, componentsMap) callbackCache.delete(event) + + // Log the event listener deletion + decrement('eventListeners', 'deleted') } }, diff --git a/src/lib/stats.js b/src/lib/stats.js new file mode 100644 index 00000000..5593e46e --- /dev/null +++ b/src/lib/stats.js @@ -0,0 +1,109 @@ +import { Log } from './log' + +/** + * Blits Stats module to track and report system statistics. + */ +const stats = { + components: { created: 0, deleted: 0, active: 0 }, + elements: { created: 0, deleted: 0, active: 0 }, + eventListeners: { created: 0, deleted: 0, active: 0 }, + timeouts: { created: 0, deleted: 0, active: 0 }, + intervals: { created: 0, deleted: 0, active: 0 }, +} + +const rollingAverages = { + components: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + elements: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + eventListeners: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + timeouts: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + intervals: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, +} + +let isLoggingEnabled = false +let loggingInterval = 10000 // Default interval in milliseconds + +/** + * Increment a specific statistic in the given category. + * No-op if logging is not enabled. + * @param {string} category - The category to update (e.g., 'components', 'elements', 'eventListeners'). + * @param {string} type - The type of statistic to update (e.g., 'created', 'deleted'). + */ +export function increment(category, type) { + if (!isLoggingEnabled) return + if (stats[category] && stats[category][type] !== undefined) { + stats[category][type]++ + if (type === 'created') stats[category].active++ + } +} + +/** + * Decrement a specific statistic in the given category. + * No-op if logging is not enabled. + * @param {string} category - The category to update (e.g., 'components', 'elements', 'eventListeners'). + * @param {string} type - The type of statistic to update (e.g., 'created', 'deleted'). + */ +export function decrement(category, type) { + if (!isLoggingEnabled) return + if (stats[category] && stats[category][type] !== undefined) { + stats[category][type]++ + if (type === 'deleted') stats[category].active-- + } +} + +function updateRollingAverage(category, currentActive, intervalInSeconds) { + const averages = rollingAverages[category] + const diff = currentActive - averages.lastActive + averages.lastActive = currentActive + + // Update rolling averages using exponential moving average formula + const oneMinFactor = intervalInSeconds / 60 + const fiveMinFactor = intervalInSeconds / 300 + const fifteenMinFactor = intervalInSeconds / 900 + + averages.oneMin = averages.oneMin * (1 - oneMinFactor) + diff * oneMinFactor + averages.fiveMin = averages.fiveMin * (1 - fiveMinFactor) + diff * fiveMinFactor + averages.fifteenMin = averages.fifteenMin * (1 - fifteenMinFactor) + diff * fifteenMinFactor + + return { + oneMin: averages.oneMin.toFixed(2), + fiveMin: averages.fiveMin.toFixed(2), + fifteenMin: averages.fifteenMin.toFixed(2), + } +} + +/** + * Log the current statistics using the internal logger. + * Includes formatted output with percentage differences and load averages. + */ +function logStats() { + const intervalInSeconds = loggingInterval / 1000 + + const formatStats = (category) => { + const { created, deleted, active } = stats[category] + const loadAverages = updateRollingAverage(category, active, intervalInSeconds) + + return `Active: ${active}, Created: ${created}, Deleted: ${deleted}, Load: ${loadAverages.oneMin}, ${loadAverages.fiveMin}, ${loadAverages.fifteenMin}` + } + + Log.info('--- System Statistics ---') + Log.info('Components:', formatStats('components')) + Log.info('Elements:', formatStats('elements')) + Log.info('Listeners:', formatStats('eventListeners')) + Log.info('Timeouts:', formatStats('timeouts')) + Log.info('Intervals:', formatStats('intervals')) + Log.info('-------------------------') +} + +/** + * Start periodic logging of system statistics. + * Logs statistics at the configured interval using the internal logger. + * Enables logging functionality. + * @param {number} [interval=10000] - The logging interval in milliseconds. + */ +export function startLogging(interval = 10000) { + if (isLoggingEnabled) return + isLoggingEnabled = true + loggingInterval = interval + logStats() + setInterval(logStats, loggingInterval) +} From 163f82ebd90d9c2f7df34cfec81431dfe75f3fa1 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 26 May 2025 11:44:33 +0200 Subject: [PATCH 02/13] feat: implement stats tracking and logging mechanism with optional overlay --- docs/built-in/stats.md | 95 ++++++++++++++++ src/component.js | 5 +- src/component/base/methods.js | 4 +- src/component/base/timeouts_intervals.js | 16 +-- src/components/StatsOverlay.js | 138 +++++++++++++++++++++++ src/components/index.js | 2 + src/engines/L3/element.js | 6 +- src/launch.js | 8 +- src/lib/eventListeners.js | 8 +- src/lib/stats.js | 96 ++++++++++++---- 10 files changed, 332 insertions(+), 46 deletions(-) create mode 100644 docs/built-in/stats.md create mode 100644 src/components/StatsOverlay.js diff --git a/docs/built-in/stats.md b/docs/built-in/stats.md new file mode 100644 index 00000000..84a2050a --- /dev/null +++ b/docs/built-in/stats.md @@ -0,0 +1,95 @@ +# Blits Stats & Performance Logging + +Blits provides a highly-performant, production-optimized stats and system logging mechanism. This feature is **entirely opt-in** and is controlled at build time using a Vite flag. When disabled, all stats code is removed from the final bundle for maximum performance. + +## Enabling Stats Logging + +To enable stats logging, set the `__BLITS_STATS__` flag in your `vite.config.js`: + +```js +import { defineConfig } from 'vite' + +export default defineConfig({ + define: { + __BLITS_STATS__: true // Set to false to disable stats (default) + }, + // ...other Vite config options +}) +``` + +- When `__BLITS_STATS__` is `true`, Blits will track and periodically log system statistics (components, elements, event listeners, timeouts, intervals, etc.). +- When `__BLITS_STATS__` is `false` (or not set), **all stats logic and function calls are optimized out** at build time. There is zero runtime cost in production. + +## How It Works + +- All stats-related calls (e.g., `increment`, `decrement`) are guarded with the `BLITS_STATS_ENABLED` flag, which is exported from `src/lib/stats.js`. +- Example usage in the codebase: + + ```js + import { increment, BLITS_STATS_ENABLED } from './lib/stats.js' + // ... + BLITS_STATS_ENABLED && increment('components', 'created') + ``` +- When the flag is `false`, these calls are removed by dead code elimination during the build. + +## No Runtime Settings + +- There is **no runtime setting** for enabling/disabling stats. The only way to enable stats is at build time using the Vite flag. +- The previous `enableStatsLogger` setting in Blits settings is now deprecated and ignored. + +## What Gets Logged? + +When enabled, Blits will periodically log: + +- Number of active, created, and deleted components +- Number of elements, event listeners, timeouts, and intervals +- Rolling load averages for each category +- Renderer memory usage information (when available) + +Example log output: + +``` +--- System Statistics --- +Components: Active: 5, Created: 10, Deleted: 5, Load: 0.10, 0.05, 0.01 +Elements: Active: 20, Created: 40, Deleted: 20, Load: 0.20, 0.10, 0.02 +Listeners: Active: 3, Created: 6, Deleted: 3, Load: 0.03, 0.01, 0.00 +Timeouts: Active: 1, Created: 2, Deleted: 1, Load: 0.01, 0.00, 0.00 +Intervals: Active: 2, Created: 4, Deleted: 2, Load: 0.02, 0.01, 0.00 +------------------------- +``` + +## Visual Stats Overlay + +Blits includes a built-in component that displays real-time stats in the top left corner of the screen: + +```js +// Import the component +import { BlitsStatsOverlay } from 'blits' + +// Add it to your root component +export default Component('App', { + template: ` + + + + + ` +}) +``` + +The `BlitsStatsOverlay` component: +- Only renders when `__BLITS_STATS__` is enabled +- Shows real-time stats for components, elements, listeners, timeouts and intervals +- Displays renderer memory information when available +- Automatically updates every 500ms +- Has zero impact when stats are disabled (the component returns null) + +## Best Practices + +- **Enable stats only for development or diagnostics builds.** +- For production, keep `__BLITS_STATS__` set to `false` for optimal performance. +- You can safely leave `increment`/`decrement` calls in your code—they will be removed from the bundle when stats are disabled. + +--- + +For more details, see the implementation in `src/lib/stats.js` and usages throughout the codebase. diff --git a/src/component.js b/src/component.js index 9b988a11..d73a96fe 100644 --- a/src/component.js +++ b/src/component.js @@ -24,7 +24,7 @@ import { reactive, getRaw } from './lib/reactivity/reactive.js' import { effect } from './lib/reactivity/effect.js' import Lifecycle from './lib/lifecycle.js' import symbols from './lib/symbols.js' -import { increment } from './lib/stats.js' +import { increment, BLITS_STATS_ENABLED } from './lib/stats.js' import { stage, renderer } from './launch.js' @@ -213,8 +213,7 @@ const Component = (name = required('name'), config = required('config')) => { // finaly set the lifecycle state to ready (in the next tick) setTimeout(() => (this.lifecycle.state = 'ready')) - // Example: Increment component creation - increment('components', 'created') + BLITS_STATS_ENABLED && increment('components', 'created') // and return this return this diff --git a/src/component/base/methods.js b/src/component/base/methods.js index 8b2a60c3..02b045ae 100644 --- a/src/component/base/methods.js +++ b/src/component/base/methods.js @@ -21,7 +21,7 @@ import eventListeners from '../../lib/eventListeners.js' import { trigger } from '../../lib/reactivity/effect.js' import { Log } from '../../lib/log.js' import { removeGlobalEffects } from '../../lib/reactivity/effect.js' -import { decrement } from '../../lib/stats.js' +import { BLITS_STATS_ENABLED, decrement } from '../../lib/stats.js' export default { focus: { @@ -88,7 +88,7 @@ export default { delete this[symbols.holder] Log.debug(`Destroyed component ${this.componentId}`) - decrement('components', 'deleted') + BLITS_STATS_ENABLED && decrement('components', 'deleted') }, writable: false, enumerable: true, diff --git a/src/component/base/timeouts_intervals.js b/src/component/base/timeouts_intervals.js index c891f3b9..c0bec847 100644 --- a/src/component/base/timeouts_intervals.js +++ b/src/component/base/timeouts_intervals.js @@ -16,7 +16,7 @@ */ import symbols from '../../lib/symbols.js' -import { increment, decrement } from '../../lib/stats.js' +import { increment, decrement, BLITS_STATS_ENABLED } from '../../lib/stats.js' export default { $setTimeout: { @@ -24,14 +24,14 @@ export default { const timeoutId = setTimeout( () => { this[symbols.timeouts] = this[symbols.timeouts].filter((id) => id !== timeoutId) - decrement('timeouts', 'deleted') + BLITS_STATS_ENABLED && decrement('timeouts', 'deleted') fn.apply(null, params) }, ms, params ) this[symbols.timeouts].push(timeoutId) - increment('timeouts', 'created') + BLITS_STATS_ENABLED && increment('timeouts', 'created') return timeoutId }, writable: false, @@ -43,7 +43,7 @@ export default { if (this[symbols.timeouts].indexOf(timeoutId) > -1) { this[symbols.timeouts] = this[symbols.timeouts].filter((id) => id !== timeoutId) clearTimeout(timeoutId) - decrement('timeouts', 'deleted') + BLITS_STATS_ENABLED && decrement('timeouts', 'deleted') } }, writable: false, @@ -54,7 +54,7 @@ export default { value: function () { for (let i = 0; i < this[symbols.timeouts].length; i++) { clearTimeout(this[symbols.timeouts][i]) - decrement('timeouts', 'deleted') + BLITS_STATS_ENABLED && decrement('timeouts', 'deleted') } this[symbols.timeouts] = [] }, @@ -66,7 +66,7 @@ export default { value: function (fn, ms, ...params) { const intervalId = setInterval(() => fn.apply(null, params), ms, params) this[symbols.intervals].push(intervalId) - increment('intervals', 'created') + BLITS_STATS_ENABLED && increment('intervals', 'created') return intervalId }, writable: false, @@ -78,7 +78,7 @@ export default { if (this[symbols.intervals].indexOf(intervalId) > -1) { this[symbols.intervals] = this[symbols.intervals].filter((id) => id !== intervalId) clearInterval(intervalId) - decrement('intervals', 'deleted') + BLITS_STATS_ENABLED && decrement('intervals', 'deleted') } }, writable: false, @@ -89,7 +89,7 @@ export default { value: function () { for (let i = 0; i < this[symbols.intervals].length; i++) { clearInterval(this[symbols.intervals][i]) - decrement('intervals', 'active') + BLITS_STATS_ENABLED && decrement('intervals', 'deleted') } this[symbols.intervals] = [] }, diff --git a/src/components/StatsOverlay.js b/src/components/StatsOverlay.js new file mode 100644 index 00000000..86186eb0 --- /dev/null +++ b/src/components/StatsOverlay.js @@ -0,0 +1,138 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import Component from '../component.js' +import { BLITS_STATS_ENABLED, getStats, bytesToMb } from '../lib/stats.js' +import { renderer } from '../launch.js' +import { Log } from '../lib/log.js' + +/** + * BlitsStatsOverlay + * Renders live system and renderer stats on screen (top left corner) + * Only displayed when BLITS_STATS_ENABLED is true + */ +export default () => + BLITS_STATS_ENABLED + ? Component('StatsOverlay', { + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + state() { + return { + statsTitle: 'System Statistics', + componentStats: 'Components: Loading...', + elementStats: 'Elements: Loading...', + listenerStats: 'Listeners: Loading...', + timeoutStats: 'Timeouts: Loading...', + intervalStats: 'Intervals: Loading...', + + hasMemoryInfo: false, + memoryTitle: 'Renderer Memory', + memoryStats1: '', + memoryStats2: '', + } + }, + hooks: { + ready() { + // Update stats every 500ms + Log.info('BlitsStatsOverlay: Starting stats update interval') + this._interval = setInterval(() => { + this.updateStats() + }, 500) + }, + destroy() { + if (this._interval) { + clearInterval(this._interval) + } + }, + }, + methods: { + updateStats() { + // Update system stats + const components = getStats('components') + const elements = getStats('elements') + const listeners = getStats('eventListeners') + const timeouts = getStats('timeouts') + const intervals = getStats('intervals') + + if (components) { + this.componentStats = `Components: Active ${components.active} | Created ${components.created} | Deleted ${components.deleted}` + } + + if (elements) { + this.elementStats = `Elements: Active ${elements.active} | Created ${elements.created} | Deleted ${elements.deleted}` + } + + if (listeners) { + this.listenerStats = `Listeners: Active ${listeners.active} | Created ${listeners.created} | Deleted ${listeners.deleted}` + } + + if (timeouts) { + this.timeoutStats = `Timeouts: Active ${timeouts.active} | Created ${timeouts.created} | Deleted ${timeouts.deleted}` + } + + if (intervals) { + this.intervalStats = `Intervals: Active ${intervals.active} | Created ${intervals.created} | Deleted ${intervals.deleted}` + } + + // Update memory info if available + const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null + if (memInfo) { + this.hasMemoryInfo = true + this.memoryStats1 = `Memory: ${bytesToMb( + memInfo.memUsed + )}MB | Renderable: ${bytesToMb(memInfo.renderableMemUsed)}MB` + this.memoryStats2 = `Target: ${bytesToMb( + memInfo.targetThreshold + )}MB | Critical: ${bytesToMb(memInfo.criticalThreshold)}MB` + + if (memInfo.loadedTextures !== undefined) { + this.memoryStats2 += ` | Textures: ${memInfo.loadedTextures}` + } + } + }, + }, + }) + : () => null diff --git a/src/components/index.js b/src/components/index.js index 3533f10b..57d89706 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -19,10 +19,12 @@ import Circle from './Circle.js' import RouterView from './RouterView.js' import Sprite from './Sprite.js' import FPScounter from './FPScounter.js' +import StatsOverlay from './StatsOverlay.js' export default () => ({ Circle: Circle(), RouterView: RouterView(), Sprite: Sprite(), FPScounter: FPScounter(), + StatsOverlay: StatsOverlay(), }) diff --git a/src/engines/L3/element.js b/src/engines/L3/element.js index dea44701..ffbfd6b6 100644 --- a/src/engines/L3/element.js +++ b/src/engines/L3/element.js @@ -17,7 +17,7 @@ import { renderer } from './launch.js' import colors from '../../lib/colors/colors.js' -import { increment, decrement } from '../../lib/stats.js' +import { increment, BLITS_STATS_ENABLED, decrement } from '../../lib/stats.js' import { Log } from '../../lib/log.js' import symbols from '../../lib/symbols.js' @@ -508,7 +508,7 @@ const Element = { } // Increment element creation - increment('elements', 'created') + BLITS_STATS_ENABLED && increment('elements', 'created') }, set(prop, value) { if (value === undefined) return @@ -643,7 +643,7 @@ const Element = { this.node = null // Decrement element deletion - decrement('elements', 'deleted') + BLITS_STATS_ENABLED && decrement('elements', 'deleted') }, get nodeId() { return this.node && this.node.id diff --git a/src/launch.js b/src/launch.js index f0e48c59..2cf37d1e 100644 --- a/src/launch.js +++ b/src/launch.js @@ -18,7 +18,7 @@ import Settings from './settings.js' import { initLog, Log } from './lib/log.js' import engine from './engine.js' -import { startLogging } from './lib/stats.js' +import { startLogging, BLITS_STATS_ENABLED } from './lib/stats.js' import blitsPackageInfo from '../package.json' assert { type: 'json' } export let renderer = {} @@ -43,9 +43,9 @@ export default (App, target, settings) => { initLog() - if (settings.enableStatsLogger) { - startLogging() - } + // Only start logging if BLITS_STATS_ENABLED is true + // This code will be optimized out if BLITS_STATS_ENABLED is false + BLITS_STATS_ENABLED && startLogging() rendererVersion().then((v) => { Log.info('Blits Version ', blitsPackageInfo.version) diff --git a/src/lib/eventListeners.js b/src/lib/eventListeners.js index 93d91b2a..db7f29b1 100644 --- a/src/lib/eventListeners.js +++ b/src/lib/eventListeners.js @@ -15,7 +15,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { increment, decrement } from './stats' +import { increment, decrement, BLITS_STATS_ENABLED } from './stats' const eventsMap = new Map() const callbackCache = new Map() @@ -38,7 +38,7 @@ export default { callbackCache.delete(event) // Invalidate the callbackCache when a new callback is added // Log the event listener creation - increment('eventListeners', 'created') + BLITS_STATS_ENABLED && increment('eventListeners', 'created') }, deregisterListener(component, event) { @@ -53,7 +53,7 @@ export default { callbackCache.delete(event) // Log the event listener deletion - decrement('eventListeners', 'deleted') + BLITS_STATS_ENABLED && decrement('eventListeners', 'deleted') } }, @@ -98,5 +98,7 @@ export default { } } } + + BLITS_STATS_ENABLED && decrement('eventListeners', 'deleted') }, } diff --git a/src/lib/stats.js b/src/lib/stats.js index 5593e46e..e05c73c0 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -1,35 +1,46 @@ import { Log } from './log' +import { renderer } from '../launch.js' + +// Vite flag for enabling stats (should be replaced at build time) +// Use globalThis to avoid TDZ and reference errors +// prettier-ignore +const __BLITS_STATS__ = typeof globalThis.__BLITS_STATS__ !== 'undefined' ? globalThis.__BLITS_STATS__ : false /** * Blits Stats module to track and report system statistics. + * Only enabled if __BLITS_STATS__ is true at build time. */ -const stats = { - components: { created: 0, deleted: 0, active: 0 }, - elements: { created: 0, deleted: 0, active: 0 }, - eventListeners: { created: 0, deleted: 0, active: 0 }, - timeouts: { created: 0, deleted: 0, active: 0 }, - intervals: { created: 0, deleted: 0, active: 0 }, -} +const stats = __BLITS_STATS__ + ? { + components: { created: 0, deleted: 0, active: 0 }, + elements: { created: 0, deleted: 0, active: 0 }, + eventListeners: { created: 0, deleted: 0, active: 0 }, + timeouts: { created: 0, deleted: 0, active: 0 }, + intervals: { created: 0, deleted: 0, active: 0 }, + } + : null -const rollingAverages = { - components: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - elements: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - eventListeners: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - timeouts: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - intervals: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, -} +const rollingAverages = __BLITS_STATS__ + ? { + components: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + elements: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + eventListeners: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + timeouts: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + intervals: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + } + : null let isLoggingEnabled = false let loggingInterval = 10000 // Default interval in milliseconds /** * Increment a specific statistic in the given category. - * No-op if logging is not enabled. + * No-op if stats are not enabled. * @param {string} category - The category to update (e.g., 'components', 'elements', 'eventListeners'). * @param {string} type - The type of statistic to update (e.g., 'created', 'deleted'). */ export function increment(category, type) { - if (!isLoggingEnabled) return + if (!__BLITS_STATS__ || !isLoggingEnabled) return if (stats[category] && stats[category][type] !== undefined) { stats[category][type]++ if (type === 'created') stats[category].active++ @@ -38,12 +49,12 @@ export function increment(category, type) { /** * Decrement a specific statistic in the given category. - * No-op if logging is not enabled. + * No-op if stats are not enabled. * @param {string} category - The category to update (e.g., 'components', 'elements', 'eventListeners'). * @param {string} type - The type of statistic to update (e.g., 'created', 'deleted'). */ export function decrement(category, type) { - if (!isLoggingEnabled) return + if (!__BLITS_STATS__ || !isLoggingEnabled) return if (stats[category] && stats[category][type] !== undefined) { stats[category][type]++ if (type === 'deleted') stats[category].active-- @@ -51,6 +62,7 @@ export function decrement(category, type) { } function updateRollingAverage(category, currentActive, intervalInSeconds) { + if (!__BLITS_STATS__) return { oneMin: 0, fiveMin: 0, fifteenMin: 0 } const averages = rollingAverages[category] const diff = currentActive - averages.lastActive averages.lastActive = currentActive @@ -71,11 +83,8 @@ function updateRollingAverage(category, currentActive, intervalInSeconds) { } } -/** - * Log the current statistics using the internal logger. - * Includes formatted output with percentage differences and load averages. - */ function logStats() { + if (!__BLITS_STATS__) return const intervalInSeconds = loggingInterval / 1000 const formatStats = (category) => { @@ -91,7 +100,22 @@ function logStats() { Log.info('Listeners:', formatStats('eventListeners')) Log.info('Timeouts:', formatStats('timeouts')) Log.info('Intervals:', formatStats('intervals')) - Log.info('-------------------------') + + const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null + if (memInfo) { + Log.info('--- Renderer Memory Info ---') + Log.info( + `Memory used: ${bytesToMb(memInfo.memUsed)} Mb, Renderable: ${bytesToMb( + memInfo.renderableMemUsed + )} Mb, Target: ${bytesToMb(memInfo.targetThreshold)} Mb, Critical: ${bytesToMb( + memInfo.criticalThreshold + )} Mb` + ) + Log.info( + `Textures loaded ${memInfo.loadedTextures}, renderable textures: ${memInfo.renderableTexturesLoaded}` + ) + Log.info('------------------------------') + } } /** @@ -101,9 +125,35 @@ function logStats() { * @param {number} [interval=10000] - The logging interval in milliseconds. */ export function startLogging(interval = 10000) { + if (!__BLITS_STATS__) return if (isLoggingEnabled) return isLoggingEnabled = true loggingInterval = interval logStats() setInterval(logStats, loggingInterval) } + +/** + * Get stats for a given category (for UI overlay). + * @param {string} category + * @returns {object|null} + */ +export function getStats(category) { + if (!__BLITS_STATS__ || !stats) return null + return stats[category] || null +} + +/** + * Format bytes to MB (for UI overlay). + * @param {number} bytes + * @returns {string} + */ +export function bytesToMb(bytes) { + return (bytes / 1024 / 1024).toFixed(2) +} + +/** + * Expose the build-time stats flag for use in other modules. + * @type {boolean} + */ +export const BLITS_STATS_ENABLED = __BLITS_STATS__ From bc7d073b6bdabe55a548668c7d43f8ab62ec9980 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 26 May 2025 13:03:48 +0200 Subject: [PATCH 03/13] fix: update StatsOverlay to conditionally render based on BLITS_STATS_ENABLED --- src/components/StatsOverlay.js | 206 +++++++++++++++++---------------- 1 file changed, 105 insertions(+), 101 deletions(-) diff --git a/src/components/StatsOverlay.js b/src/components/StatsOverlay.js index 86186eb0..d6911b13 100644 --- a/src/components/StatsOverlay.js +++ b/src/components/StatsOverlay.js @@ -26,113 +26,117 @@ import { Log } from '../lib/log.js' * Only displayed when BLITS_STATS_ENABLED is true */ export default () => - BLITS_STATS_ENABLED - ? Component('StatsOverlay', { - template: ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - + Component('StatsOverlay', { + template: ` + + + + + - `, - state() { - return { - statsTitle: 'System Statistics', - componentStats: 'Components: Loading...', - elementStats: 'Elements: Loading...', - listenerStats: 'Listeners: Loading...', - timeoutStats: 'Timeouts: Loading...', - intervalStats: 'Intervals: Loading...', + + + + + + + + + + + + + + + + + + + + + + + + `, + state() { + return { + statsEnabled: BLITS_STATS_ENABLED, + statsTitle: 'System Statistics', + componentStats: 'Components: Loading...', + elementStats: 'Elements: Loading...', + listenerStats: 'Listeners: Loading...', + timeoutStats: 'Timeouts: Loading...', + intervalStats: 'Intervals: Loading...', - hasMemoryInfo: false, - memoryTitle: 'Renderer Memory', - memoryStats1: '', - memoryStats2: '', - } - }, - hooks: { - ready() { - // Update stats every 500ms - Log.info('BlitsStatsOverlay: Starting stats update interval') - this._interval = setInterval(() => { - this.updateStats() - }, 500) - }, - destroy() { - if (this._interval) { - clearInterval(this._interval) - } - }, - }, - methods: { - updateStats() { - // Update system stats - const components = getStats('components') - const elements = getStats('elements') - const listeners = getStats('eventListeners') - const timeouts = getStats('timeouts') - const intervals = getStats('intervals') + hasMemoryInfo: false, + memoryTitle: 'Renderer Memory', + memoryStats1: '', + memoryStats2: '', + } + }, + hooks: { + ready() { + // Only start the interval if stats are enabled + if (BLITS_STATS_ENABLED) { + Log.info('BlitsStatsOverlay: Starting stats update interval') + this._interval = this.$setInterval(() => { + this.updateStats() + }, 500) + } + }, + destroy() { + if (this._interval) { + this.$clearInterval(this._interval) + } + }, + }, + methods: { + updateStats() { + // Skip if stats are disabled + if (!BLITS_STATS_ENABLED) return - if (components) { - this.componentStats = `Components: Active ${components.active} | Created ${components.created} | Deleted ${components.deleted}` - } + // Update system stats + const components = getStats('components') + const elements = getStats('elements') + const listeners = getStats('eventListeners') + const timeouts = getStats('timeouts') + const intervals = getStats('intervals') - if (elements) { - this.elementStats = `Elements: Active ${elements.active} | Created ${elements.created} | Deleted ${elements.deleted}` - } + if (components) { + this.componentStats = `Components: Active ${components.active} | Created ${components.created} | Deleted ${components.deleted}` + } - if (listeners) { - this.listenerStats = `Listeners: Active ${listeners.active} | Created ${listeners.created} | Deleted ${listeners.deleted}` - } + if (elements) { + this.elementStats = `Elements: Active ${elements.active} | Created ${elements.created} | Deleted ${elements.deleted}` + } - if (timeouts) { - this.timeoutStats = `Timeouts: Active ${timeouts.active} | Created ${timeouts.created} | Deleted ${timeouts.deleted}` - } + if (listeners) { + this.listenerStats = `Listeners: Active ${listeners.active} | Created ${listeners.created} | Deleted ${listeners.deleted}` + } - if (intervals) { - this.intervalStats = `Intervals: Active ${intervals.active} | Created ${intervals.created} | Deleted ${intervals.deleted}` - } + if (timeouts) { + this.timeoutStats = `Timeouts: Active ${timeouts.active} | Created ${timeouts.created} | Deleted ${timeouts.deleted}` + } - // Update memory info if available - const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null - if (memInfo) { - this.hasMemoryInfo = true - this.memoryStats1 = `Memory: ${bytesToMb( - memInfo.memUsed - )}MB | Renderable: ${bytesToMb(memInfo.renderableMemUsed)}MB` - this.memoryStats2 = `Target: ${bytesToMb( - memInfo.targetThreshold - )}MB | Critical: ${bytesToMb(memInfo.criticalThreshold)}MB` + if (intervals) { + this.intervalStats = `Intervals: Active ${intervals.active} | Created ${intervals.created} | Deleted ${intervals.deleted}` + } - if (memInfo.loadedTextures !== undefined) { - this.memoryStats2 += ` | Textures: ${memInfo.loadedTextures}` - } - } - }, - }, - }) - : () => null + // Update memory info if available + const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null + if (memInfo) { + this.hasMemoryInfo = true + this.memoryStats1 = `Memory: ${bytesToMb(memInfo.memUsed)}MB | Renderable: ${bytesToMb( + memInfo.renderableMemUsed + )}MB` + this.memoryStats2 = `Target: ${bytesToMb( + memInfo.targetThreshold + )}MB | Critical: ${bytesToMb(memInfo.criticalThreshold)}MB` + + if (memInfo.loadedTextures !== undefined) { + this.memoryStats2 += ` | Textures: ${memInfo.loadedTextures}` + } + } + }, + }, + }) From 74f93a2770f38305318484961d6f527a29719955 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 26 May 2025 13:10:23 +0200 Subject: [PATCH 04/13] fix: update import statement for Log module to include file extension --- src/lib/stats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/stats.js b/src/lib/stats.js index e05c73c0..f297b387 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -1,4 +1,4 @@ -import { Log } from './log' +import { Log } from './log.js' import { renderer } from '../launch.js' // Vite flag for enabling stats (should be replaced at build time) From c48fda11bb8e4ec045a64f12386cce91d1f9630b Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 26 May 2025 18:24:35 +0200 Subject: [PATCH 05/13] feat: Implement Blits Stats for debugging --- docs/_sidebar.md | 1 + docs/built-in/stats.md | 27 +++++++++++++-------------- src/lib/eventListeners.js | 2 +- src/lib/stats.js | 30 ++++++++++++++++++------------ 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 89586c92..caa717c9 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -24,6 +24,7 @@ - [Directives](/built-in/directives.md) - [For loop](/built-in/for-loop.md) - [Layout](/built-in/layout.md) + - [Stats](/built-in/stats.md) - Router - [Basics](/router/basics.md) - [Hooks](/router/hooks.md) diff --git a/docs/built-in/stats.md b/docs/built-in/stats.md index 84a2050a..5ec5c6e0 100644 --- a/docs/built-in/stats.md +++ b/docs/built-in/stats.md @@ -1,6 +1,8 @@ # Blits Stats & Performance Logging -Blits provides a highly-performant, production-optimized stats and system logging mechanism. This feature is **entirely opt-in** and is controlled at build time using a Vite flag. When disabled, all stats code is removed from the final bundle for maximum performance. +Blits provides a highly-performant stats and system logging mechanism. This feature is **entirely opt-in** and is controlled at build time using a Vite flag. When disabled, all stats code is removed from the final bundle for maximum performance. + +Ideally this is only enabled in development or test builds for performance validation. ## Enabling Stats Logging @@ -34,8 +36,7 @@ export default defineConfig({ ## No Runtime Settings -- There is **no runtime setting** for enabling/disabling stats. The only way to enable stats is at build time using the Vite flag. -- The previous `enableStatsLogger` setting in Blits settings is now deprecated and ignored. +There is **no runtime setting** for enabling/disabling stats. The only way to enable stats is at build time using the Vite flag. ## What Gets Logged? @@ -55,6 +56,9 @@ Elements: Active: 20, Created: 40, Deleted: 20, Load: 0.20, 0.10, 0.02 Listeners: Active: 3, Created: 6, Deleted: 3, Load: 0.03, 0.01, 0.00 Timeouts: Active: 1, Created: 2, Deleted: 1, Load: 0.01, 0.00, 0.00 Intervals: Active: 2, Created: 4, Deleted: 2, Load: 0.02, 0.01, 0.00 +--- Renderer Memory Info --- +Memory used: 27.27 Mb, Renderable: 25.62 Mb, Target: 160.00 Mb, Critical: 200.00 Mb +Textures loaded 6, renderable textures: 3 ------------------------- ``` @@ -64,32 +68,27 @@ Blits includes a built-in component that displays real-time stats in the top lef ```js // Import the component -import { BlitsStatsOverlay } from 'blits' +import { StatsOverlay } from 'blits' // Add it to your root component export default Component('App', { template: ` - + ` }) ``` -The `BlitsStatsOverlay` component: -- Only renders when `__BLITS_STATS__` is enabled -- Shows real-time stats for components, elements, listeners, timeouts and intervals +The `StatsOverlay` component: +- Only updates when `__BLITS_STATS__` is enabled +- Shows real-time stats for components, elements, listeners, timeouts and intervals as well as texture and memory usage statistics - Displays renderer memory information when available - Automatically updates every 500ms -- Has zero impact when stats are disabled (the component returns null) +- Has zero impact when stats are disabled ## Best Practices - **Enable stats only for development or diagnostics builds.** - For production, keep `__BLITS_STATS__` set to `false` for optimal performance. -- You can safely leave `increment`/`decrement` calls in your code—they will be removed from the bundle when stats are disabled. - ---- - -For more details, see the implementation in `src/lib/stats.js` and usages throughout the codebase. diff --git a/src/lib/eventListeners.js b/src/lib/eventListeners.js index db7f29b1..d739fee1 100644 --- a/src/lib/eventListeners.js +++ b/src/lib/eventListeners.js @@ -15,7 +15,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { increment, decrement, BLITS_STATS_ENABLED } from './stats' +import { increment, decrement, BLITS_STATS_ENABLED } from './stats.js' const eventsMap = new Map() const callbackCache = new Map() diff --git a/src/lib/stats.js b/src/lib/stats.js index f297b387..59c55f74 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -83,23 +83,29 @@ function updateRollingAverage(category, currentActive, intervalInSeconds) { } } +/** + * Format stats for a specific category for display. + * @param {string} category - The category to format stats for. + * @param {number} intervalInSeconds - The interval in seconds for rolling average calculation. + * @returns {string} - Formatted stats string. + */ +function formatStats(category, intervalInSeconds) { + const { created, deleted, active } = stats[category] + const loadAverages = updateRollingAverage(category, active, intervalInSeconds) + + return `Active: ${active}, Created: ${created}, Deleted: ${deleted}, Load: ${loadAverages.oneMin}, ${loadAverages.fiveMin}, ${loadAverages.fifteenMin}` +} + function logStats() { if (!__BLITS_STATS__) return const intervalInSeconds = loggingInterval / 1000 - const formatStats = (category) => { - const { created, deleted, active } = stats[category] - const loadAverages = updateRollingAverage(category, active, intervalInSeconds) - - return `Active: ${active}, Created: ${created}, Deleted: ${deleted}, Load: ${loadAverages.oneMin}, ${loadAverages.fiveMin}, ${loadAverages.fifteenMin}` - } - Log.info('--- System Statistics ---') - Log.info('Components:', formatStats('components')) - Log.info('Elements:', formatStats('elements')) - Log.info('Listeners:', formatStats('eventListeners')) - Log.info('Timeouts:', formatStats('timeouts')) - Log.info('Intervals:', formatStats('intervals')) + Log.info('Components:', formatStats('components', intervalInSeconds)) + Log.info('Elements:', formatStats('elements', intervalInSeconds)) + Log.info('Listeners:', formatStats('eventListeners', intervalInSeconds)) + Log.info('Timeouts:', formatStats('timeouts', intervalInSeconds)) + Log.info('Intervals:', formatStats('intervals', intervalInSeconds)) const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null if (memInfo) { From 737db13941fefedc1194845b6480494b1e8b3f04 Mon Sep 17 00:00:00 2001 From: erikhaandrikman Date: Mon, 26 May 2025 23:30:21 +0200 Subject: [PATCH 06/13] feat: add force reset and print to stats --- src/application.js | 11 +++++++++++ src/lib/stats.js | 46 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/application.js b/src/application.js index b6e38ac4..fdd11c96 100644 --- a/src/application.js +++ b/src/application.js @@ -22,6 +22,8 @@ import Settings from './settings.js' import symbols from './lib/symbols.js' import { DEFAULT_HOLD_TIMEOUT_MS } from './constants.js' +import { BLITS_STATS_ENABLED, resetStats, printStats } from './lib/stats.js' + const Application = (config) => { const defaultKeyMap = { ArrowLeft: 'left', @@ -60,6 +62,15 @@ const Application = (config) => { keyDownHandler = async (e) => { const key = keyMap[e.key] || keyMap[e.keyCode] || e.key || e.keyCode + + if (BLITS_STATS_ENABLED) { + if (key === 'p') { + printStats() + } else if (key === 'r') { + resetStats() + } + } + // intercept key press if specified in main Application component if ( this[symbols.inputEvents] !== undefined && diff --git a/src/lib/stats.js b/src/lib/stats.js index 59c55f74..a4137e96 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -98,14 +98,8 @@ function formatStats(category, intervalInSeconds) { function logStats() { if (!__BLITS_STATS__) return - const intervalInSeconds = loggingInterval / 1000 - Log.info('--- System Statistics ---') - Log.info('Components:', formatStats('components', intervalInSeconds)) - Log.info('Elements:', formatStats('elements', intervalInSeconds)) - Log.info('Listeners:', formatStats('eventListeners', intervalInSeconds)) - Log.info('Timeouts:', formatStats('timeouts', intervalInSeconds)) - Log.info('Intervals:', formatStats('intervals', intervalInSeconds)) + printStats() const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null if (memInfo) { @@ -120,7 +114,43 @@ function logStats() { Log.info( `Textures loaded ${memInfo.loadedTextures}, renderable textures: ${memInfo.renderableTexturesLoaded}` ) - Log.info('------------------------------') + } +} + +const formatStats = (category) => { + const { created, deleted, active } = stats[category] + return `Active: ${active}, Created: ${created}, Deleted: ${deleted}` +} + +export function printStats() { + if (!__BLITS_STATS__) return + + Log.info('------------------------------') + Log.info('--- System Statistics ---') + Log.info('URL: ', window.location.href) + Log.info('Components:', formatStats('components')) + Log.info('Elements:', formatStats('elements')) + Log.info('Listeners:', formatStats('eventListeners')) + Log.info('Timeouts:', formatStats('timeouts')) + Log.info('Intervals:', formatStats('intervals')) +} + +export function resetStats() { + if (!__BLITS_STATS__) return + for (const category in stats) { + if (Object.prototype.hasOwnProperty.call(stats, category)) { + stats[category].created = 0 + stats[category].deleted = 0 + stats[category].active = 0 + } + } + for (const category in rollingAverages) { + if (Object.prototype.hasOwnProperty.call(rollingAverages, category)) { + rollingAverages[category].oneMin = 0 + rollingAverages[category].fiveMin = 0 + rollingAverages[category].fifteenMin = 0 + rollingAverages[category].lastActive = 0 + } } } From 78713f20231b47ead617679f4d19a2cef96e20ff Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 26 May 2025 18:24:35 +0200 Subject: [PATCH 07/13] feat: Implement Blits Stats for debugging --- src/lib/stats.js | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/src/lib/stats.js b/src/lib/stats.js index a4137e96..dd9b0311 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -114,43 +114,7 @@ function logStats() { Log.info( `Textures loaded ${memInfo.loadedTextures}, renderable textures: ${memInfo.renderableTexturesLoaded}` ) - } -} - -const formatStats = (category) => { - const { created, deleted, active } = stats[category] - return `Active: ${active}, Created: ${created}, Deleted: ${deleted}` -} - -export function printStats() { - if (!__BLITS_STATS__) return - - Log.info('------------------------------') - Log.info('--- System Statistics ---') - Log.info('URL: ', window.location.href) - Log.info('Components:', formatStats('components')) - Log.info('Elements:', formatStats('elements')) - Log.info('Listeners:', formatStats('eventListeners')) - Log.info('Timeouts:', formatStats('timeouts')) - Log.info('Intervals:', formatStats('intervals')) -} - -export function resetStats() { - if (!__BLITS_STATS__) return - for (const category in stats) { - if (Object.prototype.hasOwnProperty.call(stats, category)) { - stats[category].created = 0 - stats[category].deleted = 0 - stats[category].active = 0 - } - } - for (const category in rollingAverages) { - if (Object.prototype.hasOwnProperty.call(rollingAverages, category)) { - rollingAverages[category].oneMin = 0 - rollingAverages[category].fiveMin = 0 - rollingAverages[category].fifteenMin = 0 - rollingAverages[category].lastActive = 0 - } + Log.info('------------------------------') } } From c108a742703fe64b359add1e9df68c4cbabb0e4e Mon Sep 17 00:00:00 2001 From: erikhaandrikman Date: Mon, 26 May 2025 23:30:21 +0200 Subject: [PATCH 08/13] feat: add force reset and print to stats --- src/lib/stats.js | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/lib/stats.js b/src/lib/stats.js index dd9b0311..a4137e96 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -114,7 +114,43 @@ function logStats() { Log.info( `Textures loaded ${memInfo.loadedTextures}, renderable textures: ${memInfo.renderableTexturesLoaded}` ) - Log.info('------------------------------') + } +} + +const formatStats = (category) => { + const { created, deleted, active } = stats[category] + return `Active: ${active}, Created: ${created}, Deleted: ${deleted}` +} + +export function printStats() { + if (!__BLITS_STATS__) return + + Log.info('------------------------------') + Log.info('--- System Statistics ---') + Log.info('URL: ', window.location.href) + Log.info('Components:', formatStats('components')) + Log.info('Elements:', formatStats('elements')) + Log.info('Listeners:', formatStats('eventListeners')) + Log.info('Timeouts:', formatStats('timeouts')) + Log.info('Intervals:', formatStats('intervals')) +} + +export function resetStats() { + if (!__BLITS_STATS__) return + for (const category in stats) { + if (Object.prototype.hasOwnProperty.call(stats, category)) { + stats[category].created = 0 + stats[category].deleted = 0 + stats[category].active = 0 + } + } + for (const category in rollingAverages) { + if (Object.prototype.hasOwnProperty.call(rollingAverages, category)) { + rollingAverages[category].oneMin = 0 + rollingAverages[category].fiveMin = 0 + rollingAverages[category].fifteenMin = 0 + rollingAverages[category].lastActive = 0 + } } } From 345799293802b64b26c9ad4a505e884520b0f5ea Mon Sep 17 00:00:00 2001 From: Michiel van der Geest Date: Tue, 27 May 2025 09:34:05 +0200 Subject: [PATCH 09/13] Added flag to indicate EOL state of component to prevent registering listeners, timeouts and intervals. --- src/component/base/events.js | 2 ++ src/component/base/methods.js | 1 + src/component/base/timeouts_intervals.js | 4 ++++ 3 files changed, 7 insertions(+) diff --git a/src/component/base/events.js b/src/component/base/events.js index fff927f5..62007c21 100644 --- a/src/component/base/events.js +++ b/src/component/base/events.js @@ -29,6 +29,8 @@ export default { }, $listen: { value: function (event, callback, priority = 0) { + // early exit when component is marked as end of life + if (this.eol === true) return eventListeners.registerListener(this, event, callback, priority) }, writable: false, diff --git a/src/component/base/methods.js b/src/component/base/methods.js index 02b045ae..a88f3127 100644 --- a/src/component/base/methods.js +++ b/src/component/base/methods.js @@ -53,6 +53,7 @@ export default { }, destroy: { value: function () { + this.eol = true this.lifecycle.state = 'destroy' this.$clearTimeouts() this.$clearIntervals() diff --git a/src/component/base/timeouts_intervals.js b/src/component/base/timeouts_intervals.js index c0bec847..05921ef7 100644 --- a/src/component/base/timeouts_intervals.js +++ b/src/component/base/timeouts_intervals.js @@ -21,6 +21,8 @@ import { increment, decrement, BLITS_STATS_ENABLED } from '../../lib/stats.js' export default { $setTimeout: { value: function (fn, ms, ...params) { + // early exit when component is marked as end of life + if (this.eol === true) return const timeoutId = setTimeout( () => { this[symbols.timeouts] = this[symbols.timeouts].filter((id) => id !== timeoutId) @@ -64,6 +66,8 @@ export default { }, $setInterval: { value: function (fn, ms, ...params) { + // early exit when component is marked as end of life + if (this.eol === true) return const intervalId = setInterval(() => fn.apply(null, params), ms, params) this[symbols.intervals].push(intervalId) BLITS_STATS_ENABLED && increment('intervals', 'created') From c9595e1ada99533217cb2b70ebf3a6240636cb67 Mon Sep 17 00:00:00 2001 From: erikhaandrikman Date: Tue, 27 May 2025 10:15:26 +0200 Subject: [PATCH 10/13] feat: update logging --- src/launch.js | 4 ++-- src/lib/stats.js | 57 ++++++++++++++---------------------------------- 2 files changed, 18 insertions(+), 43 deletions(-) diff --git a/src/launch.js b/src/launch.js index 2cf37d1e..30436cdd 100644 --- a/src/launch.js +++ b/src/launch.js @@ -18,7 +18,7 @@ import Settings from './settings.js' import { initLog, Log } from './lib/log.js' import engine from './engine.js' -import { startLogging, BLITS_STATS_ENABLED } from './lib/stats.js' +import { enableLogging, BLITS_STATS_ENABLED } from './lib/stats.js' import blitsPackageInfo from '../package.json' assert { type: 'json' } export let renderer = {} @@ -45,7 +45,7 @@ export default (App, target, settings) => { // Only start logging if BLITS_STATS_ENABLED is true // This code will be optimized out if BLITS_STATS_ENABLED is false - BLITS_STATS_ENABLED && startLogging() + BLITS_STATS_ENABLED && enableLogging() rendererVersion().then((v) => { Log.info('Blits Version ', blitsPackageInfo.version) diff --git a/src/lib/stats.js b/src/lib/stats.js index a4137e96..474f972b 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -20,18 +20,7 @@ const stats = __BLITS_STATS__ } : null -const rollingAverages = __BLITS_STATS__ - ? { - components: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - elements: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - eventListeners: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - timeouts: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - intervals: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - } - : null - let isLoggingEnabled = false -let loggingInterval = 10000 // Default interval in milliseconds /** * Increment a specific statistic in the given category. @@ -98,23 +87,7 @@ function formatStats(category, intervalInSeconds) { function logStats() { if (!__BLITS_STATS__) return - printStats() - - const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null - if (memInfo) { - Log.info('--- Renderer Memory Info ---') - Log.info( - `Memory used: ${bytesToMb(memInfo.memUsed)} Mb, Renderable: ${bytesToMb( - memInfo.renderableMemUsed - )} Mb, Target: ${bytesToMb(memInfo.targetThreshold)} Mb, Critical: ${bytesToMb( - memInfo.criticalThreshold - )} Mb` - ) - Log.info( - `Textures loaded ${memInfo.loadedTextures}, renderable textures: ${memInfo.renderableTexturesLoaded}` - ) - } } const formatStats = (category) => { @@ -133,6 +106,21 @@ export function printStats() { Log.info('Listeners:', formatStats('eventListeners')) Log.info('Timeouts:', formatStats('timeouts')) Log.info('Intervals:', formatStats('intervals')) + + const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null + if (memInfo) { + Log.info('--- Renderer Memory Info ---') + Log.info( + `Memory used: ${bytesToMb(memInfo.memUsed)} Mb, Renderable: ${bytesToMb( + memInfo.renderableMemUsed + )} Mb, Target: ${bytesToMb(memInfo.targetThreshold)} Mb, Critical: ${bytesToMb( + memInfo.criticalThreshold + )} Mb` + ) + Log.info( + `Textures loaded ${memInfo.loadedTextures}, renderable textures: ${memInfo.renderableTexturesLoaded}` + ) + } } export function resetStats() { @@ -144,29 +132,16 @@ export function resetStats() { stats[category].active = 0 } } - for (const category in rollingAverages) { - if (Object.prototype.hasOwnProperty.call(rollingAverages, category)) { - rollingAverages[category].oneMin = 0 - rollingAverages[category].fiveMin = 0 - rollingAverages[category].fifteenMin = 0 - rollingAverages[category].lastActive = 0 - } - } } /** - * Start periodic logging of system statistics. - * Logs statistics at the configured interval using the internal logger. * Enables logging functionality. - * @param {number} [interval=10000] - The logging interval in milliseconds. */ -export function startLogging(interval = 10000) { +export function enableLogging() { if (!__BLITS_STATS__) return if (isLoggingEnabled) return isLoggingEnabled = true - loggingInterval = interval logStats() - setInterval(logStats, loggingInterval) } /** From 7ddaa79779a359a6567bc5135212fcbd38ce5804 Mon Sep 17 00:00:00 2001 From: wouterlucas Date: Mon, 26 May 2025 18:24:35 +0200 Subject: [PATCH 11/13] feat: Implement Blits Stats for debugging --- src/launch.js | 4 ++-- src/lib/stats.js | 30 ++++++++++++++++++------------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/launch.js b/src/launch.js index 30436cdd..2cf37d1e 100644 --- a/src/launch.js +++ b/src/launch.js @@ -18,7 +18,7 @@ import Settings from './settings.js' import { initLog, Log } from './lib/log.js' import engine from './engine.js' -import { enableLogging, BLITS_STATS_ENABLED } from './lib/stats.js' +import { startLogging, BLITS_STATS_ENABLED } from './lib/stats.js' import blitsPackageInfo from '../package.json' assert { type: 'json' } export let renderer = {} @@ -45,7 +45,7 @@ export default (App, target, settings) => { // Only start logging if BLITS_STATS_ENABLED is true // This code will be optimized out if BLITS_STATS_ENABLED is false - BLITS_STATS_ENABLED && enableLogging() + BLITS_STATS_ENABLED && startLogging() rendererVersion().then((v) => { Log.info('Blits Version ', blitsPackageInfo.version) diff --git a/src/lib/stats.js b/src/lib/stats.js index 474f972b..ba22998d 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -20,7 +20,18 @@ const stats = __BLITS_STATS__ } : null +const rollingAverages = __BLITS_STATS__ + ? { + components: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + elements: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + eventListeners: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + timeouts: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + intervals: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, + } + : null + let isLoggingEnabled = false +let loggingInterval = 10000 // Default interval in milliseconds /** * Increment a specific statistic in the given category. @@ -120,28 +131,23 @@ export function printStats() { Log.info( `Textures loaded ${memInfo.loadedTextures}, renderable textures: ${memInfo.renderableTexturesLoaded}` ) - } -} - -export function resetStats() { - if (!__BLITS_STATS__) return - for (const category in stats) { - if (Object.prototype.hasOwnProperty.call(stats, category)) { - stats[category].created = 0 - stats[category].deleted = 0 - stats[category].active = 0 - } + Log.info('------------------------------') } } /** + * Start periodic logging of system statistics. + * Logs statistics at the configured interval using the internal logger. * Enables logging functionality. + * @param {number} [interval=10000] - The logging interval in milliseconds. */ -export function enableLogging() { +export function startLogging(interval = 10000) { if (!__BLITS_STATS__) return if (isLoggingEnabled) return isLoggingEnabled = true + loggingInterval = interval logStats() + setInterval(logStats, loggingInterval) } /** From c5e2ad871eb0e67d43635d68829956a1e8556bb5 Mon Sep 17 00:00:00 2001 From: erikhaandrikman Date: Mon, 26 May 2025 23:30:21 +0200 Subject: [PATCH 12/13] feat: add force reset and print to stats --- src/lib/stats.js | 47 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/lib/stats.js b/src/lib/stats.js index ba22998d..a4137e96 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -98,7 +98,23 @@ function formatStats(category, intervalInSeconds) { function logStats() { if (!__BLITS_STATS__) return + printStats() + + const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null + if (memInfo) { + Log.info('--- Renderer Memory Info ---') + Log.info( + `Memory used: ${bytesToMb(memInfo.memUsed)} Mb, Renderable: ${bytesToMb( + memInfo.renderableMemUsed + )} Mb, Target: ${bytesToMb(memInfo.targetThreshold)} Mb, Critical: ${bytesToMb( + memInfo.criticalThreshold + )} Mb` + ) + Log.info( + `Textures loaded ${memInfo.loadedTextures}, renderable textures: ${memInfo.renderableTexturesLoaded}` + ) + } } const formatStats = (category) => { @@ -117,21 +133,24 @@ export function printStats() { Log.info('Listeners:', formatStats('eventListeners')) Log.info('Timeouts:', formatStats('timeouts')) Log.info('Intervals:', formatStats('intervals')) +} - const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null - if (memInfo) { - Log.info('--- Renderer Memory Info ---') - Log.info( - `Memory used: ${bytesToMb(memInfo.memUsed)} Mb, Renderable: ${bytesToMb( - memInfo.renderableMemUsed - )} Mb, Target: ${bytesToMb(memInfo.targetThreshold)} Mb, Critical: ${bytesToMb( - memInfo.criticalThreshold - )} Mb` - ) - Log.info( - `Textures loaded ${memInfo.loadedTextures}, renderable textures: ${memInfo.renderableTexturesLoaded}` - ) - Log.info('------------------------------') +export function resetStats() { + if (!__BLITS_STATS__) return + for (const category in stats) { + if (Object.prototype.hasOwnProperty.call(stats, category)) { + stats[category].created = 0 + stats[category].deleted = 0 + stats[category].active = 0 + } + } + for (const category in rollingAverages) { + if (Object.prototype.hasOwnProperty.call(rollingAverages, category)) { + rollingAverages[category].oneMin = 0 + rollingAverages[category].fiveMin = 0 + rollingAverages[category].fifteenMin = 0 + rollingAverages[category].lastActive = 0 + } } } From 9f4460cbf4ebe80228b2637716425bce9022379d Mon Sep 17 00:00:00 2001 From: erikhaandrikman Date: Tue, 27 May 2025 10:15:26 +0200 Subject: [PATCH 13/13] feat: update logging --- src/launch.js | 4 +-- src/lib/stats.js | 90 ++++++++---------------------------------------- 2 files changed, 17 insertions(+), 77 deletions(-) diff --git a/src/launch.js b/src/launch.js index 2cf37d1e..30436cdd 100644 --- a/src/launch.js +++ b/src/launch.js @@ -18,7 +18,7 @@ import Settings from './settings.js' import { initLog, Log } from './lib/log.js' import engine from './engine.js' -import { startLogging, BLITS_STATS_ENABLED } from './lib/stats.js' +import { enableLogging, BLITS_STATS_ENABLED } from './lib/stats.js' import blitsPackageInfo from '../package.json' assert { type: 'json' } export let renderer = {} @@ -45,7 +45,7 @@ export default (App, target, settings) => { // Only start logging if BLITS_STATS_ENABLED is true // This code will be optimized out if BLITS_STATS_ENABLED is false - BLITS_STATS_ENABLED && startLogging() + BLITS_STATS_ENABLED && enableLogging() rendererVersion().then((v) => { Log.info('Blits Version ', blitsPackageInfo.version) diff --git a/src/lib/stats.js b/src/lib/stats.js index a4137e96..bcaeb7c5 100644 --- a/src/lib/stats.js +++ b/src/lib/stats.js @@ -20,18 +20,7 @@ const stats = __BLITS_STATS__ } : null -const rollingAverages = __BLITS_STATS__ - ? { - components: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - elements: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - eventListeners: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - timeouts: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - intervals: { oneMin: 0, fiveMin: 0, fifteenMin: 0, lastActive: 0 }, - } - : null - let isLoggingEnabled = false -let loggingInterval = 10000 // Default interval in milliseconds /** * Increment a specific statistic in the given category. @@ -61,45 +50,27 @@ export function decrement(category, type) { } } -function updateRollingAverage(category, currentActive, intervalInSeconds) { - if (!__BLITS_STATS__) return { oneMin: 0, fiveMin: 0, fifteenMin: 0 } - const averages = rollingAverages[category] - const diff = currentActive - averages.lastActive - averages.lastActive = currentActive - - // Update rolling averages using exponential moving average formula - const oneMinFactor = intervalInSeconds / 60 - const fiveMinFactor = intervalInSeconds / 300 - const fifteenMinFactor = intervalInSeconds / 900 - - averages.oneMin = averages.oneMin * (1 - oneMinFactor) + diff * oneMinFactor - averages.fiveMin = averages.fiveMin * (1 - fiveMinFactor) + diff * fiveMinFactor - averages.fifteenMin = averages.fifteenMin * (1 - fifteenMinFactor) + diff * fifteenMinFactor - - return { - oneMin: averages.oneMin.toFixed(2), - fiveMin: averages.fiveMin.toFixed(2), - fifteenMin: averages.fifteenMin.toFixed(2), - } +function logStats() { + if (!__BLITS_STATS__) return + printStats() } -/** - * Format stats for a specific category for display. - * @param {string} category - The category to format stats for. - * @param {number} intervalInSeconds - The interval in seconds for rolling average calculation. - * @returns {string} - Formatted stats string. - */ -function formatStats(category, intervalInSeconds) { +const formatStats = (category) => { const { created, deleted, active } = stats[category] - const loadAverages = updateRollingAverage(category, active, intervalInSeconds) - - return `Active: ${active}, Created: ${created}, Deleted: ${deleted}, Load: ${loadAverages.oneMin}, ${loadAverages.fiveMin}, ${loadAverages.fifteenMin}` + return `Active: ${active}, Created: ${created}, Deleted: ${deleted}` } -function logStats() { +export function printStats() { if (!__BLITS_STATS__) return - printStats() + Log.info('------------------------------') + Log.info('--- System Statistics ---') + Log.info('URL: ', window.location.href) + Log.info('Components:', formatStats('components')) + Log.info('Elements:', formatStats('elements')) + Log.info('Listeners:', formatStats('eventListeners')) + Log.info('Timeouts:', formatStats('timeouts')) + Log.info('Intervals:', formatStats('intervals')) const memInfo = renderer?.stage?.txMemManager.getMemoryInfo() || null if (memInfo) { @@ -117,24 +88,6 @@ function logStats() { } } -const formatStats = (category) => { - const { created, deleted, active } = stats[category] - return `Active: ${active}, Created: ${created}, Deleted: ${deleted}` -} - -export function printStats() { - if (!__BLITS_STATS__) return - - Log.info('------------------------------') - Log.info('--- System Statistics ---') - Log.info('URL: ', window.location.href) - Log.info('Components:', formatStats('components')) - Log.info('Elements:', formatStats('elements')) - Log.info('Listeners:', formatStats('eventListeners')) - Log.info('Timeouts:', formatStats('timeouts')) - Log.info('Intervals:', formatStats('intervals')) -} - export function resetStats() { if (!__BLITS_STATS__) return for (const category in stats) { @@ -144,29 +97,16 @@ export function resetStats() { stats[category].active = 0 } } - for (const category in rollingAverages) { - if (Object.prototype.hasOwnProperty.call(rollingAverages, category)) { - rollingAverages[category].oneMin = 0 - rollingAverages[category].fiveMin = 0 - rollingAverages[category].fifteenMin = 0 - rollingAverages[category].lastActive = 0 - } - } } /** - * Start periodic logging of system statistics. - * Logs statistics at the configured interval using the internal logger. * Enables logging functionality. - * @param {number} [interval=10000] - The logging interval in milliseconds. */ -export function startLogging(interval = 10000) { +export function enableLogging() { if (!__BLITS_STATS__) return if (isLoggingEnabled) return isLoggingEnabled = true - loggingInterval = interval logStats() - setInterval(logStats, loggingInterval) } /**