Skip to content

Commit

Permalink
Cleanup timestamp-query example
Browse files Browse the repository at this point in the history
This was a great example. I started this change as just
removing a `console.log` that seemed like it shouldn't
be there but then I noticed the code didn't actually
handle the case when 'timestamp-query' doesn't exist
and ended up making a bunch of changes.

I didn't mean to step on anyone's toes. Here's the
change though.

1. Remove `console.log` from timestamp query sample

   This was just filling all of memory with strings.
   Probably not a big deal since no one is likely to
   run the sample for hours but still....

   Maybe add a `<pre>` spot that holds the last N queries
   if you want to see a history?

2. no need for `hasOngoingTimestampReadback`

   `GPUBuffer` already has `mapState` which tells you this state
   so no need to track it separately.

3. There's no need for timestampCount

   It's already on `GPUQuerySet.count`

4. code didn't check if timestamps are supported before adding
   `timestamp` section to the render pass so the sample got a validation
   error if timestamp-query was not avaialble.

   Note: you can test this with the webgpu-dev-extension. Type
   'timestamp-query' into the "Block Feature" field and
   reload the page.

5. Refactor not to use `readAsync`

   This seems like a problematic API. You call `readAsync`
   and if the map is pending then your callback is ignored.
   Ideally, if a callback was the right API design, then
   every callback you pass should get called and if it's pending
   then it would need to add your callback to a list of callbacks
   to be called when it's ready.

   Further, the timestamps passed back are only valid
   during the callback. If you tried to keep them they'd
   magically disappear as soon as your callback exited
   since they're directly the mapped buffer which will be
   unmapped as soon as you exit.

   Further, I'd argue for this sample at least, all you want is
   the last timing.

   So, changed the API to just give you the last times.
   No callbacks.

6. It's not clear what TimestampQueryManager was really managing.

   It wasn't adding the timestamps for you.
   You had to add them yourself and pull out the timestamp
   query object from internal fields inside TimestampQueryManager.

   Further, because you were adding them yourself you'd
   have to check yourself, if queries were supported then
   add them.

   Then, beacuse you were adding them yourself it was up to you
   to choose start and ends indices.

   It was also passing out BigInt but BitInt is hard to use correctly.
   I'd argue you don't need it. IIUC, Number.MAX_SAFE_INTEGER is 104 days
   of nanoseconds so it's fine to convert and give the user
   something easy to work with.

   So, I changed it to take a pairId and then use that
   to set both start and end. That way it can auto-subtract the
   pairs and give you Number. The user doesn't have to deal with
   BigInt.

Other: I considered even getting rid of pairId and have `addTimestampWrite`
just auto increment the indices to use. It would then reset to 0 when you
call `update`. With that, it could easily add up the times
and give you the total time across passes.

That way you wouldn't have to manage the pairIds yourself. On the otherhand,
if you wanted individual pass timings you need to know which pass got which
pairId. It could pass the pairId back from `addTimestamp` but I was less sure
about that change.

Anyway, I hope this is considered an improvement.
  • Loading branch information
greggman committed Nov 15, 2024
1 parent f8ed72a commit 20cf82d
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 52 deletions.
64 changes: 40 additions & 24 deletions sample/timestampQuery/TimestampQueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ export default class TimestampQueryManager {
// class does nothing.
timestampSupported: boolean;

// Number of timestamp counters
timestampCount: number;

// The query objects. This is meant to be used in a ComputePassDescriptor's
// or RenderPassDescriptor's 'timestampWrites' field.
timestampQuerySet: GPUQuerySet;
Expand All @@ -17,37 +14,51 @@ export default class TimestampQueryManager {
// A buffer to map this result back to CPU
timestampMapBuffer: GPUBuffer;

// State used to avoid firing concurrent readback of timestamp values
hasOngoingTimestampReadback: boolean;
// Last times
timestamps: number[];

// Device must have the "timestamp-query" feature
constructor(device: GPUDevice, timestampCount: number) {
constructor(device: GPUDevice, timestampPairCount: number) {
this.timestampSupported = device.features.has('timestamp-query');
if (!this.timestampSupported) return;

this.timestampCount = timestampCount;
this.timestamps = Array(timestampPairCount).fill(0);

// Create timestamp queries
this.timestampQuerySet = device.createQuerySet({
type: 'timestamp',
count: timestampCount, // begin and end
count: timestampPairCount * 2, // begin and end
});

// Create a buffer where to store the result of GPU queries
const timestampByteSize = 8; // timestamps are uint64
const timestampBufferSize = timestampCount * timestampByteSize;
this.timestampBuffer = device.createBuffer({
size: timestampBufferSize,
size: this.timestampQuerySet.count * timestampByteSize,
usage: GPUBufferUsage.COPY_SRC | GPUBufferUsage.QUERY_RESOLVE,
});

// Create a buffer to map the result back to the CPU
this.timestampMapBuffer = device.createBuffer({
size: timestampBufferSize,
size: this.timestampBuffer.size,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
}

this.hasOngoingTimestampReadback = false;
// Add both a start and end timestamp.
addTimestampWrite(
renderPassDescriptor: GPURenderPassDescriptor,
pairId: number
) {
if (this.timestampSupported) {
// We instruct the render pass to write to the timestamp query before/after
const ndx = pairId * 2;
renderPassDescriptor.timestampWrites = {
querySet: this.timestampQuerySet,
beginningOfPassWriteIndex: ndx,
endOfPassWriteIndex: ndx + 1,
};
}
return renderPassDescriptor;
}

// Resolve all timestamp queries and copy the result into the map buffer
Expand All @@ -59,13 +70,13 @@ export default class TimestampQueryManager {
commandEncoder.resolveQuerySet(
this.timestampQuerySet,
0 /* firstQuery */,
this.timestampCount /* queryCount */,
this.timestampQuerySet.count /* queryCount */,
this.timestampBuffer,
0 /* destinationOffset */
);

if (!this.hasOngoingTimestampReadback) {
// Copy values to the mapped buffer
if (this.timestampMapBuffer.mapState === 'unmapped') {
// Copy values to the mappable buffer
commandEncoder.copyBufferToBuffer(
this.timestampBuffer,
0,
Expand All @@ -76,22 +87,27 @@ export default class TimestampQueryManager {
}
}

// Once resolved, we can read back the value of timestamps
readAsync(onTimestampReadBack: (timestamps: BigUint64Array) => void): void {
// Read the value of timestamps.
update(): void {
if (!this.timestampSupported) return;
if (this.hasOngoingTimestampReadback) return;

this.hasOngoingTimestampReadback = true;
if (this.timestampMapBuffer.mapState !== 'unmapped') return;

const buffer = this.timestampMapBuffer;
void buffer.mapAsync(GPUMapMode.READ).then(() => {
const rawData = buffer.getMappedRange();
const timestamps = new BigUint64Array(rawData);

onTimestampReadBack(timestamps);

for (let i = 0; i < this.timestamps.length; ++i) {
const ndx = i * 2;
// Cast into number. Number can be 9007199254740991 as max integer
// which is 109 days of nano seconds.
const elapsedNs = Number(timestamps[ndx + 1] - timestamps[ndx]);
// It's possible elapsedNs is negative which means it's invalid
// (see spec https://gpuweb.github.io/gpuweb/#timestamp)
if (elapsedNs >= 0) {
this.timestamps[i] = elapsedNs;
}
}
buffer.unmap();
this.hasOngoingTimestampReadback = false;
});
}
}
39 changes: 11 additions & 28 deletions sample/timestampQuery/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,10 @@ const renderPassDescriptor: GPURenderPassDescriptor = {
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
// We instruct the render pass to write to the timestamp query before/after
timestampWrites: {
querySet: timestampQueryManager.timestampQuerySet,
beginningOfPassWriteIndex: 0,
endOfPassWriteIndex: 1,
},
};

timestampQueryManager.addTimestampWrite(renderPassDescriptor, 0);

const aspect = canvas.width / canvas.height;
const projectionMatrix = mat4.perspective((2 * Math.PI) / 5, aspect, 1, 100.0);
const modelViewProjectionMatrix = mat4.create();
Expand Down Expand Up @@ -222,32 +218,19 @@ function frame() {
passEncoder.end();

// Resolve timestamp queries, so that their result is available in
// a GPU-sude buffer.
// a GPU-side buffer.
timestampQueryManager.resolveAll(commandEncoder);

device.queue.submit([commandEncoder.finish()]);

// Read timestamp value back from GPU buffers
timestampQueryManager.readAsync((timestamps) => {
// This may happen (see spec https://gpuweb.github.io/gpuweb/#timestamp)
if (timestamps[1] < timestamps[0]) return;

// Measure difference (in bigints)
const elapsedNs = timestamps[1] - timestamps[0];
// Cast into regular int (ok because value is small after difference)
// and convert from nanoseconds to milliseconds:
const elapsedMs = Number(elapsedNs) * 1e-6;
renderPassDurationCounter.addSample(elapsedMs);
console.log(
'timestamps (ms): elapsed',
elapsedMs,
'avg',
renderPassDurationCounter.getAverage()
);
perfDisplay.innerHTML = `Render Pass duration: ${renderPassDurationCounter
.getAverage()
.toFixed(3)} ms ± ${renderPassDurationCounter.getStddev().toFixed(3)} ms`;
});
timestampQueryManager.update();
const elapsedNs = timestampQueryManager.timestamps[0];
// Convert from nanoseconds to milliseconds:
const elapsedMs = Number(elapsedNs) * 1e-6;
renderPassDurationCounter.addSample(elapsedMs);
perfDisplay.innerHTML = `Render Pass duration: ${renderPassDurationCounter
.getAverage()
.toFixed(3)} ms ± ${renderPassDurationCounter.getStddev().toFixed(3)} ms`;

requestAnimationFrame(frame);
}
Expand Down

0 comments on commit 20cf82d

Please sign in to comment.