Skip to content
Closed
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
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {type Source, type SourceClass, addSourceType} from './source/source.ts';
import {addProtocol, removeProtocol} from './source/protocol_crud.ts';
import {type Dispatcher, getGlobalDispatcher} from './util/dispatcher.ts';
import {EdgeInsets, type PaddingOptions} from './geo/edge_insets.ts';
import {type MapTerrainEvent, type MapStyleImageMissingEvent, type MapStyleDataEvent, type MapSourceDataEvent, type MapLibreZoomEvent, type MapLibreEvent, type MapLayerTouchEvent, type MapLayerMouseEvent, type MapLayerEventType, type MapEventType, type MapDataEvent, type MapContextEvent, MapWheelEvent, MapTouchEvent, MapMouseEvent, type MapSourceDataType, type MapProjectionEvent} from './ui/events.ts';
import {type MapTerrainEvent, type MapStyleImageMissingEvent, type MapStyleDataEvent, type MapSourceDataEvent, type MapLibreZoomEvent, type MapLibreEvent, type MapLibreRenderEvent, type MapLibreRenderTiming, type MapLayerTouchEvent, type MapLayerMouseEvent, type MapLayerEventType, type MapEventType, type MapDataEvent, type MapContextEvent, MapWheelEvent, MapTouchEvent, MapMouseEvent, type MapSourceDataType, type MapProjectionEvent} from './ui/events.ts';
import {BoxZoomHandler, type BoxZoomEndHandler, type BoxZoomHandlerOptions} from './ui/handler/box_zoom.ts';
import {DragRotateHandler} from './ui/handler/shim/drag_rotate.ts';
import {DragPanHandler, type DragPanOptions} from './ui/handler/shim/drag_pan.ts';
Expand Down Expand Up @@ -366,6 +366,8 @@ export {
type MapSourceDataEvent,
type MapLibreZoomEvent,
type MapLibreEvent,
type MapLibreRenderEvent,
type MapLibreRenderTiming,
type MapLayerTouchEvent,
type MapLayerMouseEvent,
type MapLayerEventType,
Expand Down
39 changes: 38 additions & 1 deletion src/ui/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,11 @@ export type MapEventType = {
* - a change to the map's style
* - a change to a GeoJSON source
* - the loading of a vector tile, GeoJSON file, glyph, or sprite
*
* The event carries a `timing` object with high-resolution CPU-side timing
* data; see {@link MapLibreRenderEvent}.
*/
render: MapLibreEvent;
render: MapLibreRenderEvent;
/**
* Fired immediately after the map has been resized.
*/
Expand Down Expand Up @@ -440,6 +443,40 @@ export type MapLibreEvent<TOrig = unknown> = {
originalEvent: TOrig;
};

/**
* Timing metadata attached to the `render` event. All timestamps are in
* `performance.now()`-space (DOMHighResTimeStamp, milliseconds since the
* page navigation timeline origin).
*
* Note: these are CPU-side timestamps. WebGL exposes no portable signal for
* GPU buffer-swap completion, so callers needing true frame readiness for
* capture/export should additionally listen for `viewporttilesloaded` (when
* available) or sample `canvas.captureStream(0)`.
*
* @group Event Related
*/
export type MapLibreRenderTiming = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you said, I think start/end/duration is better

/** When `painter.render()` was entered. */
start: number;
/**
* When `painter.render()` returned and all WebGL commands for the frame
* were submitted to the driver. Equivalent to `start + duration`.
*/
end: number;
/** Wall-clock time spent inside `painter.render()`, in milliseconds. */
duration: number;
};

/**
* The render event. Carries CPU-side timing information for the just-completed
* frame (see {@link MapLibreRenderTiming}).
*
* @group Event Related
*/
export type MapLibreRenderEvent = MapLibreEvent & {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Man.. this is a mess, I now see that some events are classes and some are types, I'm not sure I even know why...
My original thought was that this event should have the timing as it's properties instead of a nested obejct, but then I got confused with the different event implementations... :-/
I don't know which is better to be honest...

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the benefit of a class over a nested object?

@CommanderStorm CommanderStorm Jun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can instanceof and can inherit.

timing: MapLibreRenderTiming;
};

/**
* The style data event
*
Expand Down
12 changes: 10 additions & 2 deletions src/ui/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3621,6 +3621,7 @@ export class Map extends Camera {
* @param paintStartTimeStamp - The time when the animation frame began executing.
*/
_render(paintStartTimeStamp: number): this {
const renderStart = performance.now();
const fadeDuration = this._idleTriggered ? this._fadeDuration : 0;

const isGlobeRendering = this.style.projection?.transitionState > 0;
Expand Down Expand Up @@ -3700,8 +3701,15 @@ export class Map extends Camera {
showPadding: this.showPadding,
anisotropicFilterPitch: this.getAnisotropicFilterPitch(),
});

this.fire(new Event('render'));
const renderEnd = performance.now();

this.fire(new Event('render', {
timing: {
start: renderStart,
end: renderEnd,
duration: renderEnd - renderStart,
},
}));

if (this.loaded() && !this._loaded) {
this._loaded = true;
Expand Down
15 changes: 15 additions & 0 deletions src/ui/map_tests/map_render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,18 @@ test('redraw', async () => {
map.redraw();
await renderPromise;
});

test('render event carries timing metadata', async () => {
const map = createMap();
await map.once('idle');

const renderPromise = map.once('render');
map.redraw();
const event = await renderPromise;

expect(event.timing).toBeDefined();
expect(event.timing.end).toBeGreaterThanOrEqual(event.timing.start);
expect(event.timing.duration).toBeCloseTo(event.timing.end - event.timing.start, 5);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this check survive CI flackiness?

expect(event.timing.duration).toBeGreaterThanOrEqual(0);
expect(event.timing.duration).toBeLessThan(1000);
});
Loading