@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
428 lines • 18.2 kB
JavaScript
import { EventState, Observable } from "../observable.js";
import { PrecisionDate } from "../precisionDate.js";
import { Tools } from "../tools.js";
import { DynamicFloat32Array } from "./dynamicFloat32Array.js";
// the initial size of our array, should be a multiple of two!
const InitialArraySize = 1800;
// three octets in a hexcode. #[AA][BB][CC], i.e. 24 bits of data.
const NumberOfBitsInHexcode = 24;
// Allows single numeral hex numbers to be appended by a 0.
const HexPadding = "0";
// header for the timestamp column
const TimestampColHeader = "timestamp";
// header for the numPoints column
const NumPointsColHeader = "numPoints";
// regex to capture all carriage returns in the string.
const CarriageReturnRegex = /\r/g;
// string to use as separator when exporting extra information along with the dataset id
const ExportedDataSeparator = "@";
/**
* The collector class handles the collection and storage of data into the appropriate array.
* The collector also handles notifying any observers of any updates.
*/
export class PerformanceViewerCollector {
/**
* The offset for when actual data values start appearing inside a slice.
*/
static get SliceDataOffset() {
return 2;
}
/**
* The offset for the value of the number of points inside a slice.
*/
static get NumberOfPointsOffset() {
return 1;
}
/**
* Handles the creation of a performance viewer collector.
* @param _scene the scene to collect on.
* @param _enabledStrategyCallbacks the list of data to collect with callbacks for initialization purposes.
*/
constructor(_scene, _enabledStrategyCallbacks) {
this._scene = _scene;
/**
* Collects data for every dataset by using the appropriate strategy. This is called every frame.
* This method will then notify all observers with the latest slice.
*/
this._collectDataAtFrame = () => {
const timestamp = PrecisionDate.Now - this._startingTimestamp;
const numPoints = this.datasets.ids.length;
// add the starting index for the slice
const numberOfIndices = this.datasets.startingIndices.itemLength;
let startingIndex = 0;
if (numberOfIndices > 0) {
const previousStartingIndex = this.datasets.startingIndices.at(numberOfIndices - 1);
startingIndex =
previousStartingIndex + this.datasets.data.at(previousStartingIndex + PerformanceViewerCollector.NumberOfPointsOffset) + PerformanceViewerCollector.SliceDataOffset;
}
this.datasets.startingIndices.push(startingIndex);
// add the first 2 items in our slice.
this.datasets.data.push(timestamp);
this.datasets.data.push(numPoints);
// add the values inside the slice.
for (const id of this.datasets.ids) {
const strategy = this._strategies.get(id);
if (!strategy) {
return;
}
this.datasets.data.push(strategy.getData());
}
if (this.datasetObservable.hasObservers()) {
const slice = [timestamp, numPoints];
for (let i = 0; i < numPoints; i++) {
slice.push(this.datasets.data.at(startingIndex + PerformanceViewerCollector.SliceDataOffset + i));
}
this.datasetObservable.notifyObservers(slice);
}
};
this.datasets = {
ids: [],
data: new DynamicFloat32Array(InitialArraySize),
startingIndices: new DynamicFloat32Array(InitialArraySize),
};
this._strategies = new Map();
this._datasetMeta = new Map();
this._eventRestoreSet = new Set();
this._customEventObservable = new Observable();
this.datasetObservable = new Observable();
this.metadataObservable = new Observable((observer) => observer.callback(this._datasetMeta, new EventState(0)));
if (_enabledStrategyCallbacks) {
this.addCollectionStrategies(..._enabledStrategyCallbacks);
}
}
/**
* Registers a custom string event which will be callable via sendEvent. This method returns an event object which will contain the id of the event.
* The user can set a value optionally, which will be used in the sendEvent method. If the value is set, we will record this value at the end of each frame,
* if not we will increment our counter and record the value of the counter at the end of each frame. The value recorded is 0 if no sendEvent method is called, within a frame.
* @param name The name of the event to register
* @param forceUpdate if the code should force add an event, and replace the last one.
* @param category the category for that event
* @returns The event registered, used in sendEvent
*/
registerEvent(name, forceUpdate, category) {
if (this._strategies.has(name) && !forceUpdate) {
return;
}
if (this._strategies.has(name) && forceUpdate) {
this._strategies.get(name)?.dispose();
this._strategies.delete(name);
}
const strategy = (scene) => {
let counter = 0;
let value = 0;
const afterRenderObserver = scene.onAfterRenderObservable.add(() => {
value = counter;
counter = 0;
});
const stringObserver = this._customEventObservable.add((eventVal) => {
if (name !== eventVal.name) {
return;
}
if (eventVal.value !== undefined) {
counter = eventVal.value;
}
else {
counter++;
}
});
return {
id: name,
getData: () => value,
dispose: () => {
scene.onAfterRenderObservable.remove(afterRenderObserver);
this._customEventObservable.remove(stringObserver);
},
};
};
const event = {
name,
};
this._eventRestoreSet.add(name);
this.addCollectionStrategies({ strategyCallback: strategy, category });
return event;
}
/**
* Lets the perf collector handle an event, occurences or event value depending on if the event.value params is set.
* @param event the event to handle an occurence for
*/
sendEvent(event) {
this._customEventObservable.notifyObservers(event);
}
/**
* This event restores all custom string events if necessary.
*/
_restoreStringEvents() {
if (this._eventRestoreSet.size !== this._customEventObservable.observers.length) {
this._eventRestoreSet.forEach((event) => {
this.registerEvent(event, true);
});
}
}
/**
* This method adds additional collection strategies for data collection purposes.
* @param strategyCallbacks the list of data to collect with callbacks.
*/
addCollectionStrategies(...strategyCallbacks) {
// eslint-disable-next-line prefer-const
for (let { strategyCallback, category, hidden } of strategyCallbacks) {
const strategy = strategyCallback(this._scene);
if (this._strategies.has(strategy.id)) {
strategy.dispose();
continue;
}
this.datasets.ids.push(strategy.id);
if (category) {
category = category.replace(new RegExp(ExportedDataSeparator, "g"), "");
}
this._datasetMeta.set(strategy.id, {
color: this._getHexColorFromId(strategy.id),
category,
hidden,
});
this._strategies.set(strategy.id, strategy);
}
this.metadataObservable.notifyObservers(this._datasetMeta);
}
/**
* Gets a 6 character hexcode representing the colour from a passed in string.
* @param id the string to get a hex code for.
* @returns a hexcode hashed from the id.
*/
_getHexColorFromId(id) {
// this first bit is just a known way of hashing a string.
let hash = 0;
for (let i = 0; i < id.length; i++) {
// (hash << 5) - hash is the same as hash * 31
hash = id.charCodeAt(i) + ((hash << 5) - hash);
}
// then we build the string octet by octet.
let hex = "#";
for (let i = 0; i < NumberOfBitsInHexcode; i += 8) {
const octet = (hash >> i) & 0xff;
const toStr = HexPadding + octet.toString(16);
hex += toStr.substring(toStr.length - 2);
}
return hex;
}
/**
* Collects and then sends the latest slice to any observers by using the appropriate strategy when the user wants.
* The slice will be of the form [timestamp, numberOfPoints, value1, value2...]
* This method does not add onto the collected data accessible via the datasets variable.
*/
getCurrentSlice() {
const timestamp = PrecisionDate.Now - this._startingTimestamp;
const numPoints = this.datasets.ids.length;
const slice = [timestamp, numPoints];
// add the values inside the slice.
for (const id of this.datasets.ids) {
const strategy = this._strategies.get(id);
if (!strategy) {
return;
}
if (this.datasetObservable.hasObservers()) {
slice.push(strategy.getData());
}
}
if (this.datasetObservable.hasObservers()) {
this.datasetObservable.notifyObservers(slice);
}
}
/**
* Updates a property for a dataset's metadata with the value provided.
* @param id the id of the dataset which needs its metadata updated.
* @param prop the property to update.
* @param value the value to update the property with.
*/
updateMetadata(id, prop, value) {
const meta = this._datasetMeta.get(id);
if (!meta) {
return;
}
meta[prop] = value;
this.metadataObservable.notifyObservers(this._datasetMeta);
}
/**
* Completely clear, data, ids, and strategies saved to this performance collector.
* @param preserveStringEventsRestore if it should preserve the string events, by default will clear string events registered when called.
*/
clear(preserveStringEventsRestore) {
this.datasets.data = new DynamicFloat32Array(InitialArraySize);
this.datasets.ids.length = 0;
this.datasets.startingIndices = new DynamicFloat32Array(InitialArraySize);
this._datasetMeta.clear();
this._strategies.forEach((strategy) => strategy.dispose());
this._strategies.clear();
if (!preserveStringEventsRestore) {
this._eventRestoreSet.clear();
}
this._hasLoadedData = false;
}
/**
* Accessor which lets the caller know if the performance collector has data loaded from a file or not!
* Call clear() to reset this value.
* @returns true if the data is loaded from a file, false otherwise.
*/
get hasLoadedData() {
return this._hasLoadedData;
}
/**
* Given a string containing file data, this function parses the file data into the datasets object.
* It returns a boolean to indicate if this object was successfully loaded with the data.
* @param data string content representing the file data.
* @param keepDatasetMeta if it should use reuse the existing dataset metadata
* @returns true if the data was successfully loaded, false otherwise.
*/
loadFromFileData(data, keepDatasetMeta) {
const lines = data
.replace(CarriageReturnRegex, "")
.split("\n")
.map((line) => line.split(",").filter((s) => s.length > 0))
.filter((line) => line.length > 0);
const timestampIndex = 0;
const numPointsIndex = PerformanceViewerCollector.NumberOfPointsOffset;
if (lines.length < 2) {
return false;
}
const parsedDatasets = {
ids: [],
data: new DynamicFloat32Array(InitialArraySize),
startingIndices: new DynamicFloat32Array(InitialArraySize),
};
// parse first line separately to populate ids!
const [firstLine, ...dataLines] = lines;
// make sure we have the correct beginning headers
if (firstLine.length < 2 || firstLine[timestampIndex] !== TimestampColHeader || firstLine[numPointsIndex] !== NumPointsColHeader) {
return false;
}
const idCategoryMap = new Map();
// populate the ids.
for (let i = PerformanceViewerCollector.SliceDataOffset; i < firstLine.length; i++) {
const [id, category] = firstLine[i].split(ExportedDataSeparator);
parsedDatasets.ids.push(id);
idCategoryMap.set(id, category);
}
let startingIndex = 0;
for (const line of dataLines) {
if (line.length < 2) {
return false;
}
const timestamp = parseFloat(line[timestampIndex]);
const numPoints = parseInt(line[numPointsIndex]);
if (isNaN(numPoints) || isNaN(timestamp)) {
return false;
}
parsedDatasets.data.push(timestamp);
parsedDatasets.data.push(numPoints);
if (numPoints + PerformanceViewerCollector.SliceDataOffset !== line.length) {
return false;
}
for (let i = PerformanceViewerCollector.SliceDataOffset; i < line.length; i++) {
const val = parseFloat(line[i]);
if (isNaN(val)) {
return false;
}
parsedDatasets.data.push(val);
}
parsedDatasets.startingIndices.push(startingIndex);
startingIndex += line.length;
}
this.datasets.ids = parsedDatasets.ids;
this.datasets.data = parsedDatasets.data;
this.datasets.startingIndices = parsedDatasets.startingIndices;
if (!keepDatasetMeta) {
this._datasetMeta.clear();
}
this._strategies.forEach((strategy) => strategy.dispose());
this._strategies.clear();
// populate metadata.
if (!keepDatasetMeta) {
for (const id of this.datasets.ids) {
const category = idCategoryMap.get(id);
this._datasetMeta.set(id, { category, color: this._getHexColorFromId(id) });
}
}
this.metadataObservable.notifyObservers(this._datasetMeta);
this._hasLoadedData = true;
return true;
}
/**
* Exports the datasets inside of the collector to a csv.
*/
exportDataToCsv() {
let csvContent = "";
// create the header line.
csvContent += `${TimestampColHeader},${NumPointsColHeader}`;
for (let i = 0; i < this.datasets.ids.length; i++) {
csvContent += `,${this.datasets.ids[i]}`;
if (this._datasetMeta) {
const meta = this._datasetMeta.get(this.datasets.ids[i]);
if (meta?.category) {
csvContent += `${ExportedDataSeparator}${meta.category}`;
}
}
}
csvContent += "\n";
// create the data lines
for (let i = 0; i < this.datasets.startingIndices.itemLength; i++) {
const startingIndex = this.datasets.startingIndices.at(i);
const timestamp = this.datasets.data.at(startingIndex);
const numPoints = this.datasets.data.at(startingIndex + PerformanceViewerCollector.NumberOfPointsOffset);
csvContent += `${timestamp},${numPoints}`;
for (let offset = 0; offset < numPoints; offset++) {
csvContent += `,${this.datasets.data.at(startingIndex + PerformanceViewerCollector.SliceDataOffset + offset)}`;
}
// add extra commas.
for (let diff = 0; diff < this.datasets.ids.length - numPoints; diff++) {
csvContent += ",";
}
csvContent += "\n";
}
const fileName = `${new Date().toISOString()}-perfdata.csv`;
Tools.Download(new Blob([csvContent], { type: "text/csv" }), fileName);
}
/**
* Starts the realtime collection of data.
* @param shouldPreserve optional boolean param, if set will preserve the dataset between calls of start.
*/
start(shouldPreserve) {
if (!shouldPreserve) {
this.datasets.data = new DynamicFloat32Array(InitialArraySize);
this.datasets.startingIndices = new DynamicFloat32Array(InitialArraySize);
this._startingTimestamp = PrecisionDate.Now;
}
else if (this._startingTimestamp === undefined) {
this._startingTimestamp = PrecisionDate.Now;
}
this._scene.onAfterRenderObservable.add(this._collectDataAtFrame);
this._restoreStringEvents();
this._isStarted = true;
}
/**
* Stops the collection of data.
*/
stop() {
this._scene.onAfterRenderObservable.removeCallback(this._collectDataAtFrame);
this._isStarted = false;
}
/**
* Returns if the perf collector has been started or not.
*/
get isStarted() {
return this._isStarted;
}
/**
* Disposes of the object
*/
dispose() {
this._scene.onAfterRenderObservable.removeCallback(this._collectDataAtFrame);
this._datasetMeta.clear();
this._strategies.forEach((strategy) => {
strategy.dispose();
});
this.datasetObservable.clear();
this.metadataObservable.clear();
this._isStarted = false;
this.datasets = null;
}
}
//# sourceMappingURL=performanceViewerCollector.js.map