Skip to content

Commit

Permalink
Merge pull request #7 from RaisinTen/add-web-support
Browse files Browse the repository at this point in the history
Add web support
  • Loading branch information
RaisinTen committed May 13, 2024
2 parents 3ffc51d + 73b3c24 commit 85674e7
Show file tree
Hide file tree
Showing 21 changed files with 7,894 additions and 13 deletions.
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

0 comments on commit 85674e7

Please sign in to comment.