-
-
Notifications
You must be signed in to change notification settings - Fork 932
fix: Don't serialize GeoJSON vector tiles from the worker to support undefined properties #6803
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
base: main
Are you sure you want to change the base?
Changes from 2 commits
bd4d6a8
db722ad
d8e4f47
619322b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,11 @@ describe('reloadTile', () => { | |
| 'geometry': { | ||
| 'type': 'Point', | ||
| 'coordinates': [0, 0] | ||
| }, | ||
| 'properties': { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably better to split this out to a different test... |
||
| 'property1': 10, | ||
| 'property2': undefined, | ||
| 'property3': false | ||
| } | ||
| }; | ||
| const tileParams = { | ||
|
|
@@ -41,27 +46,36 @@ describe('reloadTile', () => { | |
| maxZoom: 10 | ||
| }; | ||
|
|
||
| await source.loadData({source: 'sourceId', data: geoJson} as LoadGeoJSONParameters); | ||
| await source.loadData({type: 'geojson', source: 'sourceId', data: geoJson} as LoadGeoJSONParameters); | ||
|
|
||
| // first call should load vector data from geojson | ||
| const firstData = await source.reloadTile(tileParams as any as WorkerTileParameters); | ||
| expect(spy).toHaveBeenCalledTimes(1); | ||
|
|
||
| // second call won't give us new rawTileData | ||
| // geojson shouldn't encode any tile data, just provide features | ||
| expect(firstData.rawTileData).toBeUndefined(); | ||
|
|
||
| // geojson should support undefined properties | ||
| expect(firstData.geoJsonFeatures[0].tags).toStrictEqual({ | ||
| 'property1': 10, | ||
| 'property2': undefined, | ||
| 'property3': false | ||
| }); | ||
|
|
||
| // second data should return the same features | ||
| let data = await source.reloadTile(tileParams as any as WorkerTileParameters); | ||
| expect('rawTileData' in data).toBeFalsy(); | ||
| data.rawTileData = firstData.rawTileData; | ||
| expect(data).toEqual(firstData); | ||
|
|
||
| // also shouldn't call loadVectorData again | ||
| expect(spy).toHaveBeenCalledTimes(1); | ||
|
|
||
| // replace geojson data | ||
| await source.loadData({source: 'sourceId', data: geoJson} as LoadGeoJSONParameters); | ||
| await source.loadData({type: 'geojson', source: 'sourceId', data: geoJson} as LoadGeoJSONParameters); | ||
|
|
||
| // should call loadVectorData again after changing geojson data | ||
| data = await source.reloadTile(tileParams as any as WorkerTileParameters); | ||
| expect('rawTileData' in data).toBeTruthy(); | ||
| expect(data.geoJsonFeatures).toBeTruthy(); | ||
| expect(data).toEqual(firstData); | ||
| expect(spy).toHaveBeenCalledTimes(2); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,6 @@ import geojsonvt, {type Options as GeoJSONVTOptions} from 'geojson-vt'; | |
| import {VectorTileWorkerSource} from './vector_tile_worker_source'; | ||
| import {createExpression} from '@maplibre/maplibre-gl-style-spec'; | ||
| import {isAbortError} from '../util/abort_error'; | ||
| import {toVirtualVectorTile} from './vector_tile_overzoomed'; | ||
| import {isUpdateableGeoJSON, type GeoJSONSourceDiff, applySourceDiff, toUpdateable, type GeoJSONFeatureId} from './geojson_source_diff'; | ||
| import type {WorkerTileParameters, WorkerTileResult} from './worker_source'; | ||
| import type {LoadVectorTileResult} from './vector_tile_worker_source'; | ||
|
|
@@ -94,7 +93,10 @@ export class GeoJSONWorkerSource extends VectorTileWorkerSource { | |
|
|
||
| const geojsonWrapper = new GeoJSONWrapper(geoJSONTile.features, {version: 2, extent: EXTENT}); | ||
|
|
||
| return toVirtualVectorTile(geojsonWrapper); | ||
| return { | ||
| vectorTile: geojsonWrapper, | ||
| isGeojson: true | ||
| }; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we convert the GeoJSON data to a vector tile here, the data is encoded as a The plain GeoJSON data cannot take advantage of the See https://developer.chrome.com/blog/transferable-objects-lightning-fast/ for more info.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tested against my performance benchmark (add 100k points, move one, https://jsbin.com/bubejanoha/edit?html,output) this branch is ~100% slower than main.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Running your tool, I'm seeing about a 15% regression on my system over an average of 40 samples in Firefox. Could you add this test to the benchmarks? I think it'd be really valuable.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've gotta step out for the afternoon but I'll try to get back to these questions in the next ~12 hrs.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Are you sure you're running
I added a
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! Prior to merging main my local branch was behind enough to not include your GeoJSONDiff benchmark. Not many great options here seeing this regression. Indirectly looking up properties after querying the feature index might be the best approach if it works.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If you pull in the latest changes from
I think we can make it work! The one big hurdle to navigate will be supporting GeoJSON sources with string feature IDs or no feature IDs. I'm happy to chat more, help brainstorm or prototype but I respect that you're the primary issue owner here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there are a couple other performance solutions worth considering...
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3 & 4 are certainly worth considering but I'm hesitant about using an older or modified version of vector-tile-js. For better or worse the current in-memory tile representation used is coupled to MVT, trying to ignore the specification (even if a minor deviation) doesn't sound too appealing to me. While I question the usefulness of undefined/null GeoJSON properties, that specification allows for them so if possible I'd prefer a solution that complies with both formats. Other options like custom serialization, a forked vector tile deserializer or tile feature culling on the main thread would all add more complexity compared to a simple sentinel, so I'm currently leaning more towards that approach. An indirect lookup from the feature index would be the runner up, with the perk of not needing a special reserved property value. @HarelM What are your thoughts on the possible approaches here? If we're fine introducing another reserved string then this whole PR becomes a lot more simple.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,11 +16,12 @@ | |
| import type {IActor} from '../util/actor'; | ||
| import type {StyleLayer} from '../style/style_layer'; | ||
| import type {StyleLayerIndex} from '../style/style_layer_index'; | ||
| import type {VectorTileLayerLike, VectorTileLike} from '@maplibre/vt-pbf'; | ||
| import {type VectorTileLayerLike, type VectorTileLike, GeoJSONWrapper} from '@maplibre/vt-pbf'; | ||
|
|
||
| export type LoadVectorTileResult = { | ||
| isGeojson?: boolean; | ||
| vectorTile: VectorTileLike; | ||
| rawData: ArrayBufferLike; | ||
| rawData?: ArrayBufferLike; | ||
| resourceTiming?: Array<PerformanceResourceTiming>; | ||
| } & ExpiryData; | ||
|
|
||
|
|
@@ -140,6 +141,7 @@ | |
| } | ||
|
|
||
| workerTile.vectorTile = response.vectorTile; | ||
| workerTile.geoJsonFeatures = response.isGeojson ? (response.vectorTile as GeoJSONWrapper).features : null; | ||
|
||
| const parsePromise = workerTile.parse(response.vectorTile, this.layerIndex, this.availableImages, this.actor, params.subdivisionGranularity); | ||
| this.loaded[tileUid] = workerTile; | ||
| // keep the original fetching state so that reload tile can pick it up if the original parse is cancelled by reloads' parse | ||
|
|
@@ -148,7 +150,11 @@ | |
| try { | ||
| const result = await parsePromise; | ||
| // Transferring a copy of rawTileData because the worker needs to retain its copy. | ||
| return extend({rawTileData: rawTileData.slice(0), encoding: params.encoding}, result, cacheControl, resourceTiming); | ||
| return extend({ | ||
| rawTileData: rawTileData?.slice(0), | ||
| geoJsonFeatures: workerTile.geoJsonFeatures, | ||
| encoding: params.encoding | ||
| }, result, cacheControl, resourceTiming); | ||
| } finally { | ||
| delete this.fetching[tileUid]; | ||
| } | ||
|
|
@@ -214,7 +220,11 @@ | |
| if (this.fetching[uid]) { | ||
| const {rawTileData, cacheControl, resourceTiming} = this.fetching[uid]; | ||
| delete this.fetching[uid]; | ||
| parseResult = extend({rawTileData: rawTileData.slice(0), encoding: params.encoding}, result, cacheControl, resourceTiming); | ||
| parseResult = extend({ | ||
| rawTileData: rawTileData?.slice(0), | ||
| geoJsonFeatures: workerTile.geoJsonFeatures, | ||
| encoding: params.encoding | ||
| }, result, cacheControl, resourceTiming); | ||
| } else { | ||
| parseResult = result; | ||
| } | ||
|
|
@@ -224,7 +234,9 @@ | |
| // if there was no vector tile data on the initial load, don't try and re-parse tile | ||
| if (workerTile.status === 'done' && workerTile.vectorTile) { | ||
| // this seems like a missing case where cache control is lost? see #3309 | ||
| return workerTile.parse(workerTile.vectorTile, this.layerIndex, this.availableImages, this.actor, params.subdivisionGranularity); | ||
| return extend({ | ||
| geoJsonFeatures: workerTile.geoJsonFeatures | ||
| }, await workerTile.parse(workerTile.vectorTile, this.layerIndex, this.availableImages, this.actor, params.subdivisionGranularity)); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,8 @@ import type {PromoteIdSpecification} from '@maplibre/maplibre-gl-style-spec'; | |
| import type {VectorTileLike} from '@maplibre/vt-pbf'; | ||
| import {type GetDashesResponse, MessageType, type GetGlyphsResponse, type GetImagesResponse} from '../util/actor_messages'; | ||
| import type {SubdivisionGranularitySetting} from '../render/subdivision_granularity_settings'; | ||
| import type {Feature} from '@maplibre/vt-pbf'; | ||
|
|
||
| export class WorkerTile { | ||
| tileID: OverscaledTileID; | ||
| uid: string | number; | ||
|
|
@@ -43,6 +45,7 @@ export class WorkerTile { | |
|
|
||
| abort: AbortController; | ||
| vectorTile: VectorTileLike; | ||
| geoJsonFeatures?: Feature[]; | ||
|
||
| inFlightDependencies: AbortController[]; | ||
|
|
||
| constructor(params: WorkerTileParameters) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How long does constructing
GeoJSONWrappertake for a source with 100k points? We used to do this step on the worker thread.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's only a wrapper, it doesn't take right...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right. I thought this was actually constructing some kind of vector tiles.