Skip to content

Commit 57cd4b1

Browse files
authored
Merge pull request #1143 from PathwayCommons/imgspd
Add controls for max. number of images to generate in parallel
2 parents 906ce07 + f2fb506 commit 57cd4b1

File tree

4 files changed

+166
-2
lines changed

4 files changed

+166
-2
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ Demo:
9797

9898
Sharing:
9999

100+
- `DOCUMENT_IMAGE_CACHE_SIZE` : number of images to cache in memory
101+
- `DOCUMENT_IMAGE_PLL_LIMIT` : max number of images to be generated in parallel (expensive)
100102
- `DOCUMENT_IMAGE_WIDTH` : tweet card image width
101103
- `DOCUMENT_IMAGE_HEIGHT` : tweet card image height
102104
- `DOCUMENT_IMAGE_PADDING` : padding around tweet card image (prevents twitter cropping issues)

src/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ export const EMAIL_ADDRESS_ADMIN = env('EMAIL_ADDRESS_ADMIN', 'pc@biofactoid.com
110110

111111
// Sharing
112112
export const DOCUMENT_IMAGE_CACHE_SIZE = env('DOCUMENT_IMAGE_CACHE_SIZE', 500);
113+
export const DOCUMENT_IMAGE_PLL_LIMIT = env('DOCUMENT_IMAGE_PLL_LIMIT', 1);
113114
export const DOCUMENT_IMAGE_WIDTH = env('DOCUMENT_IMAGE_WIDTH', 2400);
114115
export const DOCUMENT_IMAGE_HEIGHT = env('DOCUMENT_IMAGE_HEIGHT', 1200);
115116
export const DOCUMENT_IMAGE_PADDING = env('DOCUMENT_IMAGE_PADDING', 50);

src/server/routes/api/document/index.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import emailRegex from 'email-regex';
1111
import url from 'url';
1212
import { URLSearchParams } from 'url';
1313
import NodeCache from 'node-cache';
14+
import pLimit from './p-limit';
1415

1516
import { tryPromise, makeStaticStylesheet, makeCyEles, truncateString } from '../../../../util';
1617
import { msgFactory, updateCorrespondence, EmailError } from '../../../email';
@@ -54,9 +55,12 @@ import { BASE_URL,
5455
BIOPAX_DOWNLOADS_PATH,
5556
BIOPAX_IDMAP_DOWNLOADS_PATH,
5657
ORCID_PUBLIC_API_BASE_URL,
57-
DOI_LINK_BASE_URL
58+
DOI_LINK_BASE_URL,
59+
DOCUMENT_IMAGE_PLL_LIMIT
5860
} from '../../../../config';
5961

62+
63+
6064
import { ENTITY_TYPE } from '../../../../model/element/entity-type';
6165
import { eLink, elink2UidList } from './pubmed/linkPubmed';
6266
import { fetchPubmed } from './pubmed/fetchPubmed';
@@ -1407,6 +1411,12 @@ const getDocumentImageBuffer = doc => {
14071411
);
14081412
};
14091413

1414+
const imgLimit = pLimit(DOCUMENT_IMAGE_PLL_LIMIT);
1415+
1416+
const getDocumentImageBufferRatedLimited = doc => {
1417+
return imgLimit(() => getDocumentImageBuffer(doc));
1418+
};
1419+
14101420
const imageCache = new LRUCache({
14111421
max: DOCUMENT_IMAGE_CACHE_SIZE
14121422
});
@@ -1448,7 +1458,7 @@ http.get('/(:id).png', function( req, res, next ){
14481458
};
14491459

14501460
const fillCache = async doc => {
1451-
const img = await getDocumentImageBuffer(doc);
1461+
const img = await getDocumentImageBufferRatedLimited(doc);
14521462
const cache = { img };
14531463
const ttl = calcTtl(doc);
14541464

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* This file is jury-rigged. It's the entire transitive code of 'p-limit'.
3+
*
4+
* - p-limit does not support CJS.
5+
* - esm does not support p-limit.
6+
* - This could be resolved in future by upgrading the entire project to use built-in ESM
7+
* in new versions of node. However, all the imports would need to be changed. It would
8+
* be a major project.
9+
*/
10+
11+
12+
13+
14+
15+
16+
17+
18+
/*
19+
How it works:
20+
`this.#head` is an instance of `Node` which keeps track of its current value and nests another instance of `Node` that keeps the value that comes after it. When a value is provided to `.enqueue()`, the code needs to iterate through `this.#head`, going deeper and deeper to find the last value. However, iterating through every single item is slow. This problem is solved by saving a reference to the last value as `this.#tail` so that it can reference it to add a new value.
21+
*/
22+
23+
class Node {
24+
value;
25+
next;
26+
27+
constructor(value) {
28+
this.value = value;
29+
}
30+
}
31+
32+
class Queue {
33+
#head;
34+
#tail;
35+
#size;
36+
37+
constructor() {
38+
this.clear();
39+
}
40+
41+
enqueue(value) {
42+
const node = new Node(value);
43+
44+
if (this.#head) {
45+
this.#tail.next = node;
46+
this.#tail = node;
47+
} else {
48+
this.#head = node;
49+
this.#tail = node;
50+
}
51+
52+
this.#size++;
53+
}
54+
55+
dequeue() {
56+
const current = this.#head;
57+
if (!current) {
58+
return;
59+
}
60+
61+
this.#head = this.#head.next;
62+
this.#size--;
63+
return current.value;
64+
}
65+
66+
clear() {
67+
this.#head = undefined;
68+
this.#tail = undefined;
69+
this.#size = 0;
70+
}
71+
72+
get size() {
73+
return this.#size;
74+
}
75+
76+
* [Symbol.iterator]() {
77+
let current = this.#head;
78+
79+
while (current) {
80+
yield current.value;
81+
current = current.next;
82+
}
83+
}
84+
}
85+
86+
export default function pLimit(concurrency) {
87+
if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
88+
throw new TypeError('Expected `concurrency` to be a number from 1 and up');
89+
}
90+
91+
const queue = new Queue();
92+
let activeCount = 0;
93+
94+
const next = () => {
95+
activeCount--;
96+
97+
if (queue.size > 0) {
98+
queue.dequeue()();
99+
}
100+
};
101+
102+
const run = async (fn, resolve, args) => {
103+
activeCount++;
104+
105+
const result = (async () => fn(...args))();
106+
107+
resolve(result);
108+
109+
try {
110+
await result;
111+
} catch {}
112+
113+
next();
114+
};
115+
116+
const enqueue = (fn, resolve, args) => {
117+
queue.enqueue(run.bind(undefined, fn, resolve, args));
118+
119+
(async () => {
120+
// This function needs to wait until the next microtask before comparing
121+
// `activeCount` to `concurrency`, because `activeCount` is updated asynchronously
122+
// when the run function is dequeued and called. The comparison in the if-statement
123+
// needs to happen asynchronously as well to get an up-to-date value for `activeCount`.
124+
await Promise.resolve();
125+
126+
if (activeCount < concurrency && queue.size > 0) {
127+
queue.dequeue()();
128+
}
129+
})();
130+
};
131+
132+
const generator = (fn, ...args) => new Promise(resolve => {
133+
enqueue(fn, resolve, args);
134+
});
135+
136+
Object.defineProperties(generator, {
137+
activeCount: {
138+
get: () => activeCount,
139+
},
140+
pendingCount: {
141+
get: () => queue.size,
142+
},
143+
clearQueue: {
144+
value: () => {
145+
queue.clear();
146+
},
147+
},
148+
});
149+
150+
return generator;
151+
}

0 commit comments

Comments
 (0)