Skip to content

Commit

Permalink
Pre-allocate XMLHttpRequest buffer
Browse files Browse the repository at this point in the history
This avoids performance problems where loading large files causes lots of buffer concatenations. Closes #1786.
  • Loading branch information
Yun Cui authored and domenic committed Apr 29, 2017
1 parent 2552a85 commit 270f288
Showing 1 changed file with 38 additions and 12 deletions.
50 changes: 38 additions & 12 deletions lib/jsdom/living/xmlhttprequest.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ module.exports = function createXMLHttpRequest(window) {
error: "",
uploadComplete: true,
abortError: false,
cookieJar: this._ownerDocument._cookieJar
cookieJar: this._ownerDocument._cookieJar,
bufferStepSize: 1 * 1024 * 1024, // pre-allocate buffer increase step size. init value is 1MB
totalReceivedChunkSize: 0
};
this.onreadystatechange = null;
}
Expand Down Expand Up @@ -186,25 +188,29 @@ module.exports = function createXMLHttpRequest(window) {
return properties.responseCache;
}
let res = "";

const responseBuffer = !properties.responseBuffer ? null :
properties.responseBuffer.slice(0, properties.totalReceivedChunkSize);

switch (this.responseType) {
case "":
case "text": {
res = this.responseText;
break;
}
case "arraybuffer": {
if (!properties.responseBuffer) {
if (!responseBuffer) {
return null;
}
res = (new Uint8Array(properties.responseBuffer)).buffer;
res = (new Uint8Array(responseBuffer)).buffer;
break;
}
case "blob": {
if (!properties.responseBuffer) {
if (!responseBuffer) {
return null;
}
const contentType = getContentType(this);
res = Blob.create([[new Uint8Array(properties.responseBuffer)], {
res = Blob.create([[new Uint8Array(responseBuffer)], {
type: contentType && contentType.toString() || ""
}]);
break;
Expand All @@ -214,14 +220,14 @@ module.exports = function createXMLHttpRequest(window) {
break;
}
case "json": {
if (this.readyState !== XMLHttpRequest.DONE || !properties.responseBuffer) {
if (this.readyState !== XMLHttpRequest.DONE || !responseBuffer) {
res = null;
}

const contentType = getContentType(this);
const fallbackEncoding = whatwgEncoding.labelToName(
contentType && contentType.get("charset") || flag.encoding);
const jsonStr = whatwgEncoding.decode(properties.responseBuffer, fallbackEncoding);
const jsonStr = whatwgEncoding.decode(responseBuffer, fallbackEncoding);

try {
res = JSON.parse(jsonStr);
Expand All @@ -246,7 +252,9 @@ module.exports = function createXMLHttpRequest(window) {
if (properties.responseTextCache) {
return properties.responseTextCache;
}
const responseBuffer = properties.responseBuffer;
const responseBuffer = !properties.responseBuffer ? null :
properties.responseBuffer.slice(0, properties.totalReceivedChunkSize);

if (!responseBuffer) {
return "";
}
Expand All @@ -270,7 +278,9 @@ module.exports = function createXMLHttpRequest(window) {
if (properties.responseXMLCache) {
return properties.responseXMLCache;
}
const responseBuffer = properties.responseBuffer;
const responseBuffer = !properties.responseBuffer ? null :
properties.responseBuffer.slice(0, properties.totalReceivedChunkSize);

if (!responseBuffer) {
return null;
}
Expand Down Expand Up @@ -680,6 +690,9 @@ module.exports = function createXMLHttpRequest(window) {
const client = xhrUtils.createClient(this);

properties.client = client;
// For new client, reset totalReceivedChunkSize and bufferStepSize
properties.totalReceivedChunkSize = 0;
properties.bufferStepSize = 1 * 1024 * 1024;

properties.origin = flag.origin;

Expand Down Expand Up @@ -904,7 +917,8 @@ module.exports = function createXMLHttpRequest(window) {
progressObj.loaded = 0;
progressObj.lengthComputable = true;
}
properties.responseBuffer = new Buffer(0);
// pre-allocate buffer.
properties.responseBuffer = Buffer.alloc(properties.bufferStepSize);
properties.responseCache = null;
properties.responseTextCache = null;
properties.responseXMLCache = null;
Expand All @@ -918,7 +932,19 @@ module.exports = function createXMLHttpRequest(window) {
});

properties.client.on("data", chunk => {
properties.responseBuffer = Buffer.concat([properties.responseBuffer, chunk]);
properties.totalReceivedChunkSize += chunk.length;
if (properties.totalReceivedChunkSize >= properties.bufferStepSize) {
properties.bufferStepSize *= 2;
while (properties.totalReceivedChunkSize >= properties.bufferStepSize) {
properties.bufferStepSize *= 2;
}
const tmpBuf = Buffer.alloc(properties.bufferStepSize);
properties.responseBuffer = Buffer.concat([properties.responseBuffer, chunk]);
properties.responseBuffer.copy(tmpBuf, 0, 0, properties.responseBuffer.length);
properties.responseBuffer = tmpBuf;
} else {
chunk.copy(properties.responseBuffer, properties.totalReceivedChunkSize - chunk.length, 0, chunk.length);
}
properties.responseCache = null;
properties.responseTextCache = null;
properties.responseXMLCache = null;
Expand All @@ -928,7 +954,7 @@ module.exports = function createXMLHttpRequest(window) {
}
xhr.dispatchEvent(new Event("readystatechange"));

if (progressObj.total !== progressObj.loaded || properties.responseBuffer.length === byteOffset) {
if (progressObj.total !== progressObj.loaded || properties.totalReceivedChunkSize === byteOffset) {
if (lastProgressReported !== progressObj.loaded) {
// This is a necessary check in the gzip case where we can be getting new data from the client, as it
// un-gzips, but no new data has been gotten from the response, so we should not fire a progress event.
Expand Down

0 comments on commit 270f288

Please sign in to comment.