Переглянути джерело

Merge pull request #49 from NASA-AMMOS/priority-queue-sort

Priority queue sort
Garrett Johnson 5 роки тому
батько
коміт
4549fda3b5

+ 16 - 0
README.md

@@ -199,6 +199,14 @@ maxJobs = 6 : number
 
 The maximum number of jobs to be processing at once.
 
+### .unloadPriorityCallback
+
+```js
+unloadPriorityCallback = null : ( item ) => Number
+```
+
+Function to derive the unload priority of the given item. Higher priority values get unloaded first.
+
 ## LRUCache
 
 Utility class for the TilesRenderer to keep track of currently used items so rendered items will not be unloaded.
@@ -227,6 +235,14 @@ unloadPercent = 0.05 : number
 
 The maximum percentage of [minSize](#minSize) to unload during a given frame.
 
+### .priorityCallback
+
+```js
+priorityCallback = null : ( item ) => Number
+```
+
+Function to derive the job priority of the given item. Higher priority values get processed first.
+
 # LICENSE
 
 The software is available under the [Apache V2.0 license](../LICENSE.txt).

+ 6 - 4
src/base/TilesRendererBase.js

@@ -10,7 +10,7 @@ import { UNLOADED, LOADING, PARSING, LOADED, FAILED } from './constants.js';
 // TODO: Make sure active state works as expected
 
 // Function for sorting the evicted LRU items. We should evict the shallowest depth first.
-const lruSort = ( a, b ) => a.__depth - b.__depth;
+const priorityCallback = tile => 1 / tile.__depth;
 
 export class TilesRendererBase {
 
@@ -44,13 +44,15 @@ export class TilesRendererBase {
 		this.fetchOptions = {};
 
 		const lruCache = new LRUCache();
-		lruCache.sortCallback = lruSort;
+		lruCache.unloadPriorityCallback = priorityCallback;
 
 		const downloadQueue = new PriorityQueue();
 		downloadQueue.maxJobs = 4;
+		downloadQueue.priorityCallback = priorityCallback;
 
 		const parseQueue = new PriorityQueue();
 		parseQueue.maxJobs = 1;
+		parseQueue.priorityCallback = priorityCallback;
 
 		this.lruCache = lruCache;
 		this.downloadQueue = downloadQueue;
@@ -329,7 +331,7 @@ export class TilesRendererBase {
 		stats.downloading ++;
 		tile.__loadAbort = controller;
 		tile.__loadingState = LOADING;
-		downloadQueue.add( tile, priority, tile => {
+		downloadQueue.add( tile, tile => {
 
 			if ( tile.__loadIndex !== loadIndex ) {
 
@@ -373,7 +375,7 @@ export class TilesRendererBase {
 				tile.__loadAbort = null;
 				tile.__loadingState = PARSING;
 
-				return parseQueue.add( buffer, priority, buffer => {
+				return parseQueue.add( buffer, buffer => {
 
 					// if it has been unloaded then the tile has been disposed
 					if ( tile.__loadIndex !== loadIndex ) {

+ 6 - 4
src/utilities/LRUCache.js

@@ -8,6 +8,7 @@ function enqueueMicrotask( callback ) {
 	Promise.resolve().then( callback );
 
 }
+
 class LRUCache {
 
 	constructor() {
@@ -22,7 +23,7 @@ class LRUCache {
 		this.itemList = [];
 		this.callbacks = new Map();
 
-		this.sortCallback = null;
+		this.unloadPriorityCallback = null;
 
 	}
 
@@ -119,11 +120,11 @@ class LRUCache {
 		const callbacks = this.callbacks;
 		const unused = itemList.length - usedSet.size;
 		const excess = itemList.length - targetSize;
-		const prioritySortCb = this.sortCallback;
+		const unloadPriorityCallback = this.unloadPriorityCallback;
 
 		if ( excess > 0 && unused > 0 ) {
 
-			if ( prioritySortCb ) {
+			if ( unloadPriorityCallback ) {
 
 				// used items should be at the end of the array
 				itemList.sort( ( a, b ) => {
@@ -138,7 +139,8 @@ class LRUCache {
 					} else if ( ! usedA && ! usedB ) {
 
 						// Use the sort function otherwise
-						return prioritySortCb( a, b );
+						// higher priority should be further to the left
+						return unloadPriorityCallback( b ) - unloadPriorityCallback( a );
 
 					} else {
 

+ 46 - 25
src/utilities/PriorityQueue.js

@@ -1,3 +1,10 @@
+// Fires at the end of the frame and before the next one
+function enqueueMicrotask( callback ) {
+
+	Promise.resolve().then( callback );
+
+}
+
 class PriorityQueue {
 
 	constructor() {
@@ -6,31 +13,41 @@ class PriorityQueue {
 		this.maxJobs = 6;
 
 		this.items = [];
+		this.callbacks = new Map();
 		this.currJobs = 0;
 		this.scheduled = false;
 
+		this.priorityCallback = () => {
+
+			throw new Error( 'PriorityQueue: PriorityCallback function not defined.' );
+
+		};
+
 	}
 
-	add( item, priority, callback ) {
+	sort() {
 
-		return new Promise( ( resolve, reject ) => {
+		const priorityCallback = this.priorityCallback;
+		const items = this.items;
+		items.sort( ( a, b ) => {
 
-			const prCallback = ( ...args ) => callback( ...args ).then( resolve ).catch( reject );
-			const items = this.items;
-			for ( let i = 0, l = items.length; i < l; i ++ ) {
+			return priorityCallback( a ) - priorityCallback( b );
 
-				const thisItem = items[ i ];
-				if ( thisItem.priority > priority ) {
+		} );
 
-					items.splice( i, 0, { priority, item, callback: prCallback } );
-					this.scheduleJobRun();
-					return;
+	}
+
+	add( item, callback ) {
+
+		return new Promise( ( resolve, reject ) => {
 
-				}
+			const prCallback = ( ...args ) => callback( ...args ).then( resolve ).catch( reject );
+			const items = this.items;
+			const callbacks = this.callbacks;
 
-			}
+			items.push( item );
+			callbacks.set( item, prCallback );
 
-			items.push( { priority, item, callback: prCallback } );
 			this.scheduleJobRun();
 
 		} );
@@ -40,15 +57,13 @@ class PriorityQueue {
 	remove( item ) {
 
 		const items = this.items;
-		for ( let i = 0, l = items.length; i < l; i ++ ) {
+		const callbacks = this.callbacks;
 
-			const thisItem = items[ i ];
-			if ( thisItem.item === item ) {
+		const index = items.indexOf( item );
+		if ( index !== - 1 ) {
 
-				items.splice( i, 1 );
-				break;
-
-			}
+			items.splice( index, 1 );
+			callbacks.delete( item );
 
 		}
 
@@ -56,13 +71,18 @@ class PriorityQueue {
 
 	tryRunJobs() {
 
+		this.sort();
+
 		const items = this.items;
+		const callbacks = this.callbacks;
 		const maxJobs = this.maxJobs;
-		while ( maxJobs > this.currJobs && items.length > 0 ) {
+		let currJobs = this.currJobs;
+		while ( maxJobs > currJobs && items.length > 0 ) {
 
-			this.currJobs ++;
-			const { item, priority, callback } = items.pop();
-			callback( item, priority )
+			currJobs ++;
+			const item = items.pop();
+			const callback = callbacks.get( item );
+			callback( item )
 				.then( () => {
 
 					this.currJobs --;
@@ -77,6 +97,7 @@ class PriorityQueue {
 				} );
 
 		}
+		this.currJobs = currJobs;
 
 	}
 
@@ -84,7 +105,7 @@ class PriorityQueue {
 
 		if ( ! this.scheduled ) {
 
-			Promise.resolve().then( () => {
+			enqueueMicrotask( () => {
 
 				this.tryRunJobs();
 				this.scheduled = false;

+ 1 - 1
test/LRUCache.test.js

@@ -61,7 +61,7 @@ describe( 'LRUCache', () => {
 	it( 'should sort before unloading', () => {
 
 		const cache = new LRUCache();
-		cache.sortCallback = ( a, b ) => b.priority - a.priority;
+		cache.unloadPriorityCallback = item => item.priority;
 		cache.minSize = 0;
 		cache.maxSize = 10;
 		cache.unloadPercent = 1;

+ 54 - 35
test/PriorityQueue.test.js

@@ -9,13 +9,14 @@ describe( 'PriorityQueue', () => {
 
 		const queue = new PriorityQueue();
 		queue.maxJobs = 6;
-		queue.add( {}, 6, () => new Promise( () => {} ) );
-		queue.add( {}, 3, () => new Promise( () => {} ) );
-		queue.add( {}, 4, () => new Promise( () => {} ) );
-		queue.add( {}, 0, () => new Promise( () => {} ) );
-		queue.add( {}, 8, () => new Promise( () => {} ) );
-		queue.add( {}, 2, () => new Promise( () => {} ) );
-		queue.add( {}, 1, () => new Promise( () => {} ) );
+		queue.priorityCallback = item => item.priority;
+		queue.add( { priority : 6 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 3 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 4 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 0 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 8 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 2 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 1 }, () => new Promise( () => {} ) );
 
 		await nextFrame();
 
@@ -24,46 +25,60 @@ describe( 'PriorityQueue', () => {
 
 	} );
 
-	it( 'should add the jobs in the correct order.', () => {
+	it( 'should run jobs in the correct order.', async () => {
+
+		const result = [];
+		const cb = item => new Promise( resolve => {
+
+			result.push( item.priority );
+			resolve();
+
+		} );
 
 		const queue = new PriorityQueue();
-		queue.add( {}, 6, () => new Promise( () => {} ) );
-		queue.add( {}, 3, () => new Promise( () => {} ) );
-		queue.add( {}, 4, () => new Promise( () => {} ) );
-		queue.add( {}, 0, () => new Promise( () => {} ) );
-		queue.add( {}, 8, () => new Promise( () => {} ) );
-		queue.add( {}, 2, () => new Promise( () => {} ) );
-		queue.add( {}, 1, () => new Promise( () => {} ) );
+		queue.maxJobs = 1;
+		queue.priorityCallback = item => item.priority;
+		queue.add( { priority : 6 }, cb );
+		queue.add( { priority : 3 }, cb );
+		queue.add( { priority : 4 }, cb );
+		queue.add( { priority : 0 }, cb );
+		queue.add( { priority : 8 }, cb );
+		queue.add( { priority : 2 }, cb );
+		queue.add( { priority : 1 }, cb );
 
-		expect( queue.items.map( item => item.priority ) ).toEqual( [ 0, 1, 2, 3, 4, 6, 8 ] );
+		await nextTick();
+
+		expect( result ).toEqual( [ 8, 6, 4, 3, 2, 1, 0 ] );
 
 	} );
 
 	it( 'should remove an item from the queue correctly.', () => {
 
-		const A = {};
-		const B = {};
-		const C = {};
-		const D = {};
+		const A = { priority : 0 };
+		const B = { priority : 1 };
+		const C = { priority : 2 };
+		const D = { priority : 3 };
 		const queue = new PriorityQueue();
-		queue.add( A, 0, () => new Promise( () => {} ) );
-		queue.add( B, 1, () => new Promise( () => {} ) );
-		queue.add( C, 2, () => new Promise( () => {} ) );
-		queue.add( D, 3, () => new Promise( () => {} ) );
+		queue.priorityCallback = item => item.priority;
+		queue.add( A, () => new Promise( () => {} ) );
+		queue.add( B, () => new Promise( () => {} ) );
+		queue.add( C, () => new Promise( () => {} ) );
+		queue.add( D, () => new Promise( () => {} ) );
+		queue.sort();
 
-		expect( queue.items.map( item => item.item ) ).toEqual( [ A, B, C, D ] );
+		expect( queue.items ).toEqual( [ A, B, C, D ] );
 
 		queue.remove( C );
-		expect( queue.items.map( item => item.item ) ).toEqual( [ A, B, D ] );
+		expect( queue.items ).toEqual( [ A, B, D ] );
 
 		queue.remove( A );
-		expect( queue.items.map( item => item.item ) ).toEqual( [ B, D ] );
+		expect( queue.items ).toEqual( [ B, D ] );
 
 		queue.remove( B );
-		expect( queue.items.map( item => item.item ) ).toEqual( [ D ] );
+		expect( queue.items ).toEqual( [ D ] );
 
 		queue.remove( D );
-		expect( queue.items.map( item => item.item ) ).toEqual( [] );
+		expect( queue.items ).toEqual( [] );
 
 	} );
 
@@ -73,15 +88,16 @@ describe( 'PriorityQueue', () => {
 		let resolveFunc = null;
 		const queue = new PriorityQueue();
 		queue.maxJobs = 1;
+		queue.priorityCallback = item => item.priority;
 
-		queue.add( {}, 1, () => new Promise( resolve => {
+		queue.add( { priority : 1 }, () => new Promise( resolve => {
 
 			resolveFunc = resolve;
 			called ++;
 
 		} ) );
 
-		queue.add( {}, 0, () => new Promise( () => {
+		queue.add( { priority : 0 }, () => new Promise( () => {
 
 			called ++;
 
@@ -105,12 +121,13 @@ describe( 'PriorityQueue', () => {
 
 	it( 'should fire the callback with the item and priority.', async () => {
 
-		const A = {};
+		const A = { priority : 100 };
 		const queue = new PriorityQueue();
-		queue.add( A, 100, ( item, priority ) => new Promise( () => {
+		queue.priorityCallback = item => item.priority;
+
+		queue.add( A, item => new Promise( () => {
 
 			expect( item ).toEqual( A );
-			expect( priority ).toEqual( 100 );
 
 		} ) );
 
@@ -121,8 +138,10 @@ describe( 'PriorityQueue', () => {
 	it( 'should return a promise that resolves from the add function.',  async () => {
 
 		const queue = new PriorityQueue();
+		queue.priorityCallback = item => item.priority;
+
 		let result = null;
-		queue.add( {}, 0, item => Promise.resolve( 1000 ) ).then( res => result = res );
+		queue.add( { priority : 0 }, item => Promise.resolve( 1000 ) ).then( res => result = res );
 
 		expect( result ).toEqual( null );