Skip to content
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

refactor: asynchronous blob backing store #10969

Merged
merged 33 commits into from
Jul 5, 2021
Merged
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ee27fca
async blob source
jimmywarting Jun 15, 2021
cdf3741
rename byteArrays to parts
jimmywarting Jun 15, 2021
bdcbfc6
fmt + lint
lucacasonato Jun 18, 2021
374aae3
add Rust infrastructure
lucacasonato Jun 18, 2021
6efc469
Adopt to rusts Blob opaque reference
jimmywarting Jun 19, 2021
0f18cb0
Merge branch 'main' into main
lucacasonato Jun 22, 2021
6a282ec
Made size private
jimmywarting Jun 22, 2021
57405b5
Merge branch 'main' into main
lucacasonato Jun 22, 2021
9fd2aa2
lint + fmt
lucacasonato Jun 22, 2021
a73e797
fix compiler error
lucacasonato Jun 22, 2021
5c02715
Solve apply
jimmywarting Jun 22, 2021
2eb5dd7
fix op names
lucacasonato Jun 22, 2021
aba0225
fix tests
lucacasonato Jun 22, 2021
d7db60b
fix blob.slice once more
jimmywarting Jun 22, 2021
23264ea
fix
lucacasonato Jun 22, 2021
aba9e34
format
jimmywarting Jun 22, 2021
2b11283
fix more things
lucacasonato Jun 23, 2021
3649c37
relativeEnd should change with the part.size - not with slice().size
jimmywarting Jun 23, 2021
3e33378
Implement Deno.customInspect for Blob
lucacasonato Jun 24, 2021
0d4978a
fix bad tests
lucacasonato Jun 24, 2021
07d2f8b
Merge remote-tracking branch 'origin/main' into jimmywarting/main
lucacasonato Jun 28, 2021
6972a31
some tests pass
lucacasonato Jun 28, 2021
1fd7d21
update wpt
lucacasonato Jun 30, 2021
8b34b75
What I believe should happen
jimmywarting Jul 2, 2021
6d7fe90
Merge remote-tracking branch 'origin/main' into jimmywarting/main
lucacasonato Jul 3, 2021
e76a01e
gc blobs
lucacasonato Jul 3, 2021
eb7de2d
typo
lucacasonato Jul 4, 2021
b698c7e
remove a comment
lucacasonato Jul 4, 2021
9e5344e
Merge remote-tracking branch 'origin/main' into jimmywarting/main
lucacasonato Jul 4, 2021
e696d50
Merge branch 'main' of github.com:jimmywarting/deno into jimmywarting…
lucacasonato Jul 4, 2021
9db1418
fix timers bench
lucacasonato Jul 4, 2021
20a679a
don't borrow across await point
lucacasonato Jul 5, 2021
2d519e9
remove hyper from deps
lucacasonato Jul 5, 2021
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
262 changes: 174 additions & 88 deletions extensions/web/09_file.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
"use strict";

((window) => {
const Deno = window.Deno;
const core = window.Deno.core;
const webidl = window.__bootstrap.webidl;

// TODO(lucacasonato): this needs to not be hardcoded and instead depend on
// host os.
const isWindows = false;
const POOL_SIZE = 65536;

/**
* @param {string} input
Expand Down Expand Up @@ -67,58 +69,57 @@
return result;
}

/**
* @param {...Uint8Array} bytesArrays
* @returns {Uint8Array}
*/
function concatUint8Arrays(...bytesArrays) {
let byteLength = 0;
for (const bytes of bytesArrays) {
byteLength += bytes.byteLength;
}
const finalBytes = new Uint8Array(byteLength);
let current = 0;
for (const bytes of bytesArrays) {
finalBytes.set(bytes, current);
current += bytes.byteLength;
/** @param {(Blob | Uint8Array)[]} parts */
async function * toIterator (parts) {
for (const part of parts) {
if (part instanceof Blob) {
yield * part.stream();
} else if (ArrayBuffer.isView(part)) {
let position = part.byteOffset;
const end = part.byteOffset + part.byteLength;
while (position !== end) {
const size = Math.min(end - position, POOL_SIZE);
const chunk = part.buffer.slice(position, position + size);
position += chunk.byteLength;
yield new Uint8Array(chunk);
}
}
}
return finalBytes;
}

/** @typedef {BufferSource | Blob | string} BlobPart */

/**
* @param {BlobPart[]} parts
* @param {string} endings
* @returns {Uint8Array}
*/
function processBlobParts(parts, endings) {
/** @type {Uint8Array[]} */
/** @type {(Uint8Array|Blob)[]} */
const bytesArrays = [];
jimmywarting marked this conversation as resolved.
Show resolved Hide resolved
let size = 0;
for (const element of parts) {
if (element instanceof ArrayBuffer) {
bytesArrays.push(new Uint8Array(element.slice(0)));
size += element.byteLength;
} else if (ArrayBuffer.isView(element)) {
const buffer = element.buffer.slice(
element.byteOffset,
element.byteOffset + element.byteLength,
);
size += element.byteLength;
bytesArrays.push(new Uint8Array(buffer));
} else if (element instanceof Blob) {
bytesArrays.push(
new Uint8Array(element[_byteSequence].buffer.slice(0)),
);
bytesArrays.push(element);
size += element.size;
} else if (typeof element === "string") {
let s = element;
if (endings == "native") {
s = convertLineEndingsToNative(s);
}
bytesArrays.push(core.encode(s));
const chunk = core.encode(endings == "native" ? convertLineEndingsToNative(element) : element);
size += chunk.byteLength;
bytesArrays.push(chunk);
} else {
throw new TypeError("Unreachable code (invalild element type)");
throw new TypeError("Unreachable code (invalid element type)");
}
}
return concatUint8Arrays(...bytesArrays);
return {bytesArrays, size};
}

/**
Expand All @@ -133,8 +134,6 @@
return normalizedType.toLowerCase();
}

const _byteSequence = Symbol("[[ByteSequence]]");

class Blob {
get [Symbol.toStringTag]() {
return "Blob";
Expand All @@ -143,8 +142,8 @@
/** @type {string} */
#type;

/** @type {Uint8Array} */
[_byteSequence];
/** @type {(Uint8Array|Blob)[]} */
#parts;

/**
* @param {BlobPart[]} blobParts
Expand All @@ -163,18 +162,20 @@

this[webidl.brand] = webidl.brand;

/** @type {Uint8Array} */
this[_byteSequence] = processBlobParts(
const {bytesArrays, size} = processBlobParts(
blobParts,
options.endings,
);
)
/** @type {Uint8Array|Blob} */
this.#parts = bytesArrays;
this[_Size] = size;
this.#type = normalizeType(options.type);
}

/** @returns {number} */
get size() {
webidl.assertBranded(this, Blob);
return this[_byteSequence].byteLength;
return this[_Size];
}

/** @returns {string} */
Expand All @@ -189,79 +190,88 @@
* @param {string} [contentType]
* @returns {Blob}
*/
slice(start, end, contentType) {
slice(start = 0, end = this.size, contentType = '') {
jimmywarting marked this conversation as resolved.
Show resolved Hide resolved
webidl.assertBranded(this, Blob);
const prefix = "Failed to execute 'slice' on 'Blob'";
if (start !== undefined) {
start = webidl.converters["long long"](start, {
clamp: true,
context: "Argument 1",
prefix,
});
}
if (end !== undefined) {
end = webidl.converters["long long"](end, {
clamp: true,
context: "Argument 2",
prefix,
});
}
if (contentType !== undefined) {
contentType = webidl.converters["DOMString"](contentType, {
context: "Argument 3",
prefix,
});
}
start = webidl.converters["long long"](start, {
clamp: true,
context: "Argument 1",
prefix,
});
end = webidl.converters["long long"](end, {
clamp: true,
context: "Argument 2",
prefix,
});
contentType = webidl.converters["DOMString"](contentType, {
context: "Argument 3",
prefix,
});

// deno-lint-ignore no-this-alias
const O = this;
/** @type {number} */
let relativeStart;
if (start === undefined) {
relativeStart = 0;
} else {
if (start < 0) {
relativeStart = Math.max(O.size + start, 0);
} else {
relativeStart = Math.min(start, O.size);
}
}
/** @type {number} */
let relativeEnd;
if (end === undefined) {
relativeEnd = O.size;
} else {
if (end < 0) {
jimmywarting marked this conversation as resolved.
Show resolved Hide resolved
relativeEnd = Math.max(O.size + end, 0);
const {size} = this;

let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size);
let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size);

const span = Math.max(relativeEnd - relativeStart, 0);
const parts = this.#parts;
const blobParts = [];
let added = 0;

for (const part of parts) {
const size = ArrayBuffer.isView(part) ? part.byteLength : part.size;
if (relativeStart && size <= relativeStart) {
// Skip the beginning and change the relative
// start & end position as we skip the unwanted parts
relativeStart -= size;
relativeEnd -= size;
} else {
relativeEnd = Math.min(end, O.size);
let chunk
if (ArrayBuffer.isView(part)) {
chunk = part.subarray(relativeStart, Math.min(size, relativeEnd));
added += chunk.byteLength
} else {
chunk = part.slice(relativeStart, Math.min(size, relativeEnd));
added += chunk.size
}
blobParts.push(chunk);
relativeStart = 0; // All next sequential parts should start at 0

// don't add the overflow to new blobParts
if (added >= span) {
break;
}
}
}

/** @type {string} */
let relativeContentType;
if (contentType === undefined) {
relativeContentType = "";
} else {
relativeContentType = normalizeType(contentType);
}
return new Blob([
O[_byteSequence].buffer.slice(relativeStart, relativeEnd),
], { type: relativeContentType });

const blob = new Blob([], {type: relativeContentType});
blob[_Size] = span;
blob.#parts = blobParts;

return blob;
}

/**
* @returns {ReadableStream<Uint8Array>}
*/
stream() {
webidl.assertBranded(this, Blob);
const bytes = this[_byteSequence];
const partIterator = toIterator(this.#parts);
const stream = new ReadableStream({
type: "bytes",
/** @param {ReadableByteStreamController} controller */
start(controller) {
const chunk = new Uint8Array(bytes.buffer.slice(0));
if (chunk.byteLength > 0) controller.enqueue(chunk);
controller.close();
async pull (controller) {
const {value} = await partIterator.next();
if (!value) return controller.close()
controller.enqueue(value);
},
});
return stream;
Expand All @@ -282,9 +292,12 @@
async arrayBuffer() {
webidl.assertBranded(this, Blob);
const stream = this.stream();
let bytes = new Uint8Array();
const bytes = new Uint8Array(this.size);
let offset = 0;

for await (const chunk of stream) {
bytes = concatUint8Arrays(bytes, chunk);
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return bytes.buffer;
}
Expand Down Expand Up @@ -333,6 +346,7 @@
);

const _Name = Symbol("[[Name]]");
const _Size = Symbol("[[Size]]");
const _LastModfied = Symbol("[[LastModified]]");

class File extends Blob {
Expand Down Expand Up @@ -406,9 +420,81 @@
],
);

/**
* This is a blob backed up by a file on the disk
* with minium requirement. Its wrapped around a Blob as a blobPart
* so you have no direct access to this.
*
* @author Jimmy Wärting
* @private
*/
class BlobDataItem extends Blob {
#path;
#start;

constructor(options) {
super();
this.#path = options.path;
this.#start = options.start;
this[_Size] = options.size;
this.lastModified = options.lastModified;
}

/**
* Slicing arguments is first validated and formatted
* to not be out of range by Blob.prototype.slice
*/
slice(start, end) {
return new BlobDataItem({
path: this.#path,
lastModified: this.lastModified,
size: end - start,
start
});
}

async * stream() {
const {mtime} = await Deno.stat(this.#path)
if (mtime > this.lastModified) {
throw new DOMException('The requested file could not be read, ' +
'typically due to permission problems that have occurred after ' +
'a reference to a file was acquired.', 'NotReadableError');
}
if (this.size) {
const r = await Deno.open(this.#path, { read: true });
let length = this.size;
await r.seek(this.#start, Deno.SeekMode.Start);
while (length) {
const p = new Uint8Array(Math.min(length, POOL_SIZE));
length -= await r.read(p);
yield p
}
}
}
}

// TODO: Make this function public
/** @returns {Promise<File>} */
async function getFile (path, type = '') {
const stat = await Deno.stat(path);
const blobDataItem = new BlobDataItem({
path,
size: stat.size,
lastModified: stat.mtime.getTime(),
start: 0
});

// TODO: import basename?
const file = new File([blobDataItem], basename(path), {
type, lastModified: blobDataItem.lastModified
});

return file;
}

window.__bootstrap.file = {
Blob,
_byteSequence,
getFile, // TODO: expose somehow? Write doc?
File,
};
})(this);