Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add web support #7

Merged
merged 12 commits into from
May 13, 2024
Merged
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
node-version: [16.x, 18.x, 20.x, 21.x]
node-version: [18.x, 20.x, 21.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand All @@ -24,4 +24,8 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Install dependencies
run: npm ci
- run: npm test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
2 changes: 1 addition & 1 deletion docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ Call `traceEvents.getEvents()` to get the PerformanceEntry objects in the [Trace
]
```

## `trackRequires(bool)`
## `trackRequires(bool)` (only available in [CommonJS](https://nodejs.org/api/modules.html#modules-commonjs-modules))

Call `trackRequires(true)` to enable tracking `require()`s and call `trackRequires(false)` to disable tracking `require()`s.
1 change: 1 addition & 0 deletions docs/examples/client-side-use-on-web-browser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
events.json
45 changes: 45 additions & 0 deletions docs/examples/client-side-use-on-web-browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Client-side use on Web Browser

## Server

The [`server.js`](server.js) Node.js program serves the files in the current directory and also serves the `perfetto` module from [`../../../index.mjs`](../../../index.mjs).

It also exposes the following endpoints:

- `/api/perftrace` (`POST`) - Reads the performance trace from the HTTP request body, writes it to a file and closes the server
- `/api/data[0-7]` (`GET`) - Serves `{"text": "<text-to-display-by-the-client>"}` after an artificial delay
- `/api/submit` (`GET`) - Serves `{"data": "<submission-button-text>"}` after an artificial delay

## Client

The `perftrace.mjs` module is `import`ed in (only works here because the server is designed to serve the file with this name):

https://github.com/RaisinTen/perftrace/blob/fbb0d21f13329eee9912373482e7821f61441621/docs/examples/client-side-use-on-web-browser/index.mjs#L1

A new `TraceEvents` object is created in:

https://github.com/RaisinTen/perftrace/blob/fbb0d21f13329eee9912373482e7821f61441621/docs/examples/client-side-use-on-web-browser/index.mjs#L4

The client displays a list of loader animations while fetching the data from the `/api/data[0-7]` endpoint in parallel and display those in:

https://github.com/RaisinTen/perftrace/blob/fbb0d21f13329eee9912373482e7821f61441621/docs/examples/client-side-use-on-web-browser/index.mjs#L10-L24

The loads are measured using the Performance Timeline APIs:

```js
performance.mark("before");
// code to measure
performance.measure("after", "before");
```

Once the data is fully loaded, a button is displayed, clicking which, sends the performance trace to the `/api/perftrace` endpoint which causes the server to write the performance trace to a file and shut down:

https://github.com/RaisinTen/perftrace/blob/fbb0d21f13329eee9912373482e7821f61441621/docs/examples/client-side-use-on-web-browser/index.mjs#L47-L61

After running `node server.js`, opening <http://localhost:8080> in your browser and clicking the `Submit trace` button, the generated `events.json` file can be opened on <https://ui.perfetto.dev> for visualization:

![](./perfetto.png)

Here's a demo:

![](./perftrace-web.gif)
8 changes: 8 additions & 0 deletions docs/examples/client-side-use-on-web-browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<script type="module" src="index.mjs"></script>
<link rel="stylesheet" href="styles.css">
</head>
<body>
</body>
</html>
82 changes: 82 additions & 0 deletions docs/examples/client-side-use-on-web-browser/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { TraceEvents } from "./perftrace.mjs";

performance.mark("begin tracing");
const traceEvents = new TraceEvents();

const listItems = [];
let submissionDiv;
const NUM_ELEMENTS = 8;

function fetchDataId(id) {
performance.mark(`before fetching data ${id}`);
return fetch(`/api/data${id}`)
.then((response) => {
return response.json();
})
.then((data) => {
listItems[id].innerHTML = "";
const p = document.createElement("p");
p.className = "data";
p.innerText = data.text;
listItems[id].appendChild(p);
performance.measure(`fetch data ${id}`, `before fetching data ${id}`);
});
}

async function fetchData() {
const dataFetches = [];
for (let i = 0; i < NUM_ELEMENTS; ++i) {
dataFetches.push(fetchDataId(i));
}
await Promise.all(dataFetches);
}

function showLoaders() {
const ul = document.createElement("ul");
for (let i = 0; i < NUM_ELEMENTS; ++i) {
const div = document.createElement("div");
div.className = "loader";
const li = document.createElement("li");
listItems.push(li);
li.appendChild(div);
ul.appendChild(li);
}
document.body.appendChild(ul);
}

function submitTrace() {
submissionDiv.innerHTML = "";
const events = traceEvents.getEvents();
traceEvents.destroy();

// post events to /api/perftrace
fetch("/api/perftrace", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify(events)
});
}

async function displayButton() {
performance.mark("creating submit button");
const fetchData = await fetch("/api/submit");
const fetchJson = await fetchData.json();
const data = fetchJson.data;
submissionDiv = document.createElement("div");
const button = document.createElement("button");
button.className = "submit";
button.textContent = data;
button.type = "button";
button.onclick = submitTrace;
submissionDiv.appendChild(button);
document.body.appendChild(submissionDiv);
performance.measure("create submit button", "creating submit button");
performance.measure("tracing", "begin tracing");
}

showLoaders();
await fetchData();
displayButton();
13 changes: 13 additions & 0 deletions docs/examples/client-side-use-on-web-browser/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions docs/examples/client-side-use-on-web-browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "client-side-use-on-web-browser",
"version": "1.0.0",
"private": true,
"description": "Client-side use on Web Browser",
"main": "server.js",
"author": "Darshan Sen",
"license": "MIT"
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
112 changes: 112 additions & 0 deletions docs/examples/client-side-use-on-web-browser/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Tiny HTTP static file server
// Refs: https://stackoverflow.com/a/29046869

const fs = require("node:fs");
const http = require("node:http");
const path = require("node:path");

http.createServer(function(request, response) {
console.log(`Request url: "${request.url}"`);

if (request.url === "/api/perftrace") {
console.log("Serving perftrace endpoint");

request.setEncoding('utf8');
let rawData = '';
request.on('data', (chunk) => { rawData += chunk; });
request.on('end', () => {
response.end();
console.log(rawData);
fs.writeFileSync("events.json", rawData);
this.close();
});
return;
}

if (request.url === "/api/submit") {
console.log("Serving submission endpoint");
setTimeout(() => {
response.end(JSON.stringify({
data: "Submit trace"
}));
}, 250);
return;
}

const data = [
{ name: "do", timeout: 270 },
{ name: "re", timeout: 311 },
{ name: "mi", timeout: 622 },
{ name: "fa", timeout: 808 },
{ name: "so", timeout: 234 },
{ name: "la", timeout: 245 },
{ name: "ti", timeout: 300 },
{ name: "to", timeout: 690 },
];

const apiDataPrefix = "/api/data";
if (request.url.startsWith(apiDataPrefix)) {
const index = request.url.slice(apiDataPrefix.length);
const value = data[index];
if (!value) {
console.log(`Error serving data ${index} endpoint`);
response.writeHead(404, { 'Content-Type': contentType });
response.end(`${index} not found`);
return;
}
console.log(`Serving data ${index} endpoint`);
setTimeout(() => {
response.end(JSON.stringify({
text: value.name,
background: value.background,
color: value.color
}));
}, value.timeout);
return;
}

let filePath = '.' + request.url;
if (filePath == './') {
filePath = './index.html';
} else if (filePath == './perftrace.mjs') {
filePath = '../../../index.mjs';
}
const extname = path.extname(filePath);
let contentType = 'text/html';
switch (extname) {
case '.js':
case '.mjs':
contentType = 'text/javascript';
break;
case '.css':
contentType = 'text/css';
break;
case '.json':
contentType = 'application/json';
break;
case '.png':
contentType = 'image/png';
break;
case '.jpg':
contentType = 'image/jpg';
break;
case '.wav':
contentType = 'audio/wav';
break;
}

fs.readFile(filePath, function(error, content) {
if (error) {
console.log(`Error serving file "${filePath}": ${error.code}`);
response.writeHead(404, { 'Content-Type': contentType });
response.end(`File not found: ${error.code}`);
return;
}

console.log(`Serving file: "${filePath}"`);
response.writeHead(200, { 'Content-Type': contentType });
response.end(content, 'utf-8');
});
}).listen(8080);

console.log("Server running at http://localhost:8080");
41 changes: 41 additions & 0 deletions docs/examples/client-side-use-on-web-browser/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
body {
background-color: mistyrose;
}

/*
* Loaded CSS
* Refs: https://www.w3schools.com/howto/howto_css_loader.asp
*/

.loader {
border: 6px solid #f3f3f3;
border-radius: 50%;
border-top: 6px solid #3498db;
width: 30px;
height: 30px;
-webkit-animation: spin 2s linear infinite; /* Safari */
animation: spin 2s linear infinite;

/* Positioning the loaders at the same position as the loading text. */
margin-top: -20px;
margin-bottom: 25px;
}

/* Safari */
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

.data {
font-size: 30px;
}

.submit {
font-size: 25px;
}
2 changes: 1 addition & 1 deletion docs/examples/tracing-async-operations/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { TraceEvents } = require("../../..");
const { TraceEvents } = require("../../../index.cjs");

const { performance } = require("node:perf_hooks");
const { writeFileSync } = require("node:fs");
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/tracing-requires/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { TraceEvents, trackRequires } = require("../../..");
const { TraceEvents, trackRequires } = require("../../../index.cjs");
const { writeFileSync } = require('fs');

const traceEvents = new TraceEvents();
Expand Down
2 changes: 1 addition & 1 deletion index.js → index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class TraceEvents {
});
});

this._observer.observe({ entryTypes: ['measure'], buffered: true });
this._observer.observe({ type: 'measure', buffered: true });
}

destroy() {
Expand Down
31 changes: 31 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export class TraceEvents {
_eventObjects;
_observer;

constructor() {
this._eventObjects = [];
this._observer = new PerformanceObserver((perfEntryList) => {
const measures = perfEntryList.getEntriesByType('measure');
measures.forEach((measure) => {
this._eventObjects.push({
name: measure.name,
cat: measure.entryType,
ph: "X",
pid: 1,
ts: Math.round(measure.startTime * 1000),
dur: Math.round(measure.duration * 1000),
});
});
});

this._observer.observe({ type: 'measure', buffered: true });
}

destroy() {
this._observer.disconnect();
}

getEvents() {
return this._eventObjects;
}
}
Loading
Loading