Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
94 changes: 94 additions & 0 deletions docs/built-in/stats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Blits Stats & Performance Logging

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

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.

## 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
--- 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
-------------------------
```

## 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 { StatsOverlay } from 'blits'

// Add it to your root component
export default Component('App', {
template: `
<Element>
<!-- Your app content -->
<StatsOverlay />
</Element>
`
})
```

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

## Best Practices

- **Enable stats only for development or diagnostics builds.**
- For production, keep `__BLITS_STATS__` set to `false` for optimal performance.
11 changes: 11 additions & 0 deletions src/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 &&
Expand Down
3 changes: 3 additions & 0 deletions src/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, BLITS_STATS_ENABLED } from './lib/stats.js'

import { stage, renderer } from './launch.js'

Expand Down Expand Up @@ -212,6 +213,8 @@ const Component = (name = required('name'), config = required('config')) => {
// finaly set the lifecycle state to ready (in the next tick)
setTimeout(() => (this.lifecycle.state = 'ready'))

BLITS_STATS_ENABLED && increment('components', 'created')

// and return this
return this
}
Expand Down
2 changes: 2 additions & 0 deletions src/component/base/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/component/base/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 { BLITS_STATS_ENABLED, decrement } from '../../lib/stats.js'

export default {
focus: {
Expand Down Expand Up @@ -52,6 +53,7 @@ export default {
},
destroy: {
value: function () {
this.eol = true
this.lifecycle.state = 'destroy'
this.$clearTimeouts()
this.$clearIntervals()
Expand Down Expand Up @@ -87,6 +89,7 @@ export default {
delete this[symbols.holder]

Log.debug(`Destroyed component ${this.componentId}`)
BLITS_STATS_ENABLED && decrement('components', 'deleted')
},
writable: false,
enumerable: true,
Expand Down
12 changes: 12 additions & 0 deletions src/component/base/timeouts_intervals.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@
*/

import symbols from '../../lib/symbols.js'
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)
BLITS_STATS_ENABLED && decrement('timeouts', 'deleted')
fn.apply(null, params)
},
ms,
params
)
this[symbols.timeouts].push(timeoutId)
BLITS_STATS_ENABLED && increment('timeouts', 'created')
return timeoutId
},
writable: false,
Expand All @@ -40,6 +45,7 @@ export default {
if (this[symbols.timeouts].indexOf(timeoutId) > -1) {
this[symbols.timeouts] = this[symbols.timeouts].filter((id) => id !== timeoutId)
clearTimeout(timeoutId)
BLITS_STATS_ENABLED && decrement('timeouts', 'deleted')
}
},
writable: false,
Expand All @@ -50,6 +56,7 @@ export default {
value: function () {
for (let i = 0; i < this[symbols.timeouts].length; i++) {
clearTimeout(this[symbols.timeouts][i])
BLITS_STATS_ENABLED && decrement('timeouts', 'deleted')
}
this[symbols.timeouts] = []
},
Expand All @@ -59,8 +66,11 @@ 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')
return intervalId
},
writable: false,
Expand All @@ -72,6 +82,7 @@ export default {
if (this[symbols.intervals].indexOf(intervalId) > -1) {
this[symbols.intervals] = this[symbols.intervals].filter((id) => id !== intervalId)
clearInterval(intervalId)
BLITS_STATS_ENABLED && decrement('intervals', 'deleted')
}
},
writable: false,
Expand All @@ -82,6 +93,7 @@ export default {
value: function () {
for (let i = 0; i < this[symbols.intervals].length; i++) {
clearInterval(this[symbols.intervals][i])
BLITS_STATS_ENABLED && decrement('intervals', 'deleted')
}
this[symbols.intervals] = []
},
Expand Down
142 changes: 142 additions & 0 deletions src/components/StatsOverlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* 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 () =>
Component('StatsOverlay', {
template: `
<Element :visible="$statsEnabled">
<Element x="5" y="5" color="black">
<Text :content="$statsTitle" fontSize="22" fontWeight="bold" color="#fff" />
<Element y="30">
<Text :content="$componentStats" fontSize="18" color="#fff" />
</Element>
<Element y="55">
<Text :content="$elementStats" fontSize="18" color="#fff" />
</Element>
<Element y="80">
<Text :content="$listenerStats" fontSize="18" color="#fff" />
</Element>
<Element y="105">
<Text :content="$timeoutStats" fontSize="18" color="#fff" />
</Element>
<Element y="130">
<Text :content="$intervalStats" fontSize="18" color="#fff" />
</Element>
<Element y="165" :visible="$hasMemoryInfo">
<Text :content="$memoryTitle" fontSize="22" fontWeight="bold" color="#fff" />
</Element>
<Element y="195" :visible="$hasMemoryInfo">
<Text :content="$memoryStats1" fontSize="18" color="#fff" />
</Element>
<Element y="220" :visible="$hasMemoryInfo">
<Text :content="$memoryStats2" fontSize="18" color="#fff" />
</Element>
</Element>
</Element>
`,
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() {
// 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

// 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}`
}
}
},
},
})
Loading