Я добавляю данные в файл журнала с помощью OPFS (Origin Private File System API браузера) и заметил, что это очень медленно.
Для записи 5 МБ в новый файл требуется ~22 мс, но также требуется ~23 мс для добавления одного байта.
Я не сохранял ничего вроде флага добавления. Мне пришлось прочитать размер файла и начать писать с этой позиции, чтобы добавить данные. Есть ли лучший способ добавления данных?
Для сравнения: в Node.js запись 5 МБ в новый файл занимает 1,3 мс, а добавление одного байта — 0,1 мс.
Тестовый код OPFS:
(async () => {
async function create5MBFile(fileHandle) {
console.time('write 5MB');
const writable = await fileHandle.createWritable();
const chunk = new Uint8Array(5 * 1024 * 1024).fill(0);
await writable.write(chunk);
await writable.close();
console.timeEnd('write 5MB');
}
async function appendByte(fileHandle) {
const byteToAppend = new Uint8Array([0]);
console.time('append 1 byte');
const file = await fileHandle.getFile();
const currentSize = file.size;
const writable = await fileHandle.createWritable({ keepExistingData: true });
await writable.write({ type: 'write', position: currentSize, data: byteToAppend });
await writable.close();
console.timeEnd('append 1 byte');
}
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('append.log', { create: true });
await create5MBFile(fileHandle);
await appendByte(fileHandle);
})();
Тест Node.js:
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'append.log');
function create5MBFileSync() {
const fileBuffer = Buffer.alloc(5 * 1024 * 1024, 0);
console.time('write 5MB');
fs.writeFileSync(filePath, fileBuffer);
console.timeEnd('write 5MB');
}
function appendByteSync() {
const byteToAppend = Buffer.from([0]);
console.time('append 1 byte');
fs.appendFileSync(filePath, byteToAppend);
console.timeEnd('append 1 byte');
}
create5MBFileSync();
appendByteSync();
Это обходной путь. Обратной стороной является то, что данные фактически не сохраняются до тех пор, пока не произойдет запись. Я выполняю пакетную обработку в реальном приложении (куски записываются, когда они достигают определенного размера или через 15 секунд после добавления первых данных), но я не хотел слишком усложнять вопрос.
Во-первых: я не вижу разумных оснований ожидать схожих профилей производительности между средами выполнения — ни при сравнении ① (синхронные API с собственными привязками файловой системы) с ② (асинхронные API с привязками к абстракции виртуализированной файловой системы). Что касается того, что доступно для OPFS: существует метод FileSystemFileHandle: createSyncAccessHandle(), доступный только в рабочих контекстах — он обеспечивает преимущества в производительности за счет синхронного доступа на основе мьютекса.
Я реализовал свой собственный тест, и у меня было около 1,5 мс на запись/добавление 1 байта. Я думаю, проблема в том, что вы используете console.time
, который включает синхронный вызов console.info
, который медленный. Вместо этого мой использует window.performance.mark
.
Похоже, keepExistingData: true
— это дорогая часть — WHATWG знает, что это не подходит для более серьёзного ввода-вывода файлов, поэтому говорят о новой опции: inPlace, которая должна ускорять добавление, потому что нет копии.
Я не сохранял ничего вроде флага добавления. Мне пришлось прочитать размер файла и начать писать с этой позиции, чтобы добавить данные. Есть ли лучший способ добавления данных?
Текущий дизайн метода createWritable отклоняется в сторону осторожности:
Возвращает
FileSystemWritableFileStream
, который можно использовать для записи в файл. Любые изменения, внесенные через поток, не будут отражены в записи файла, которую можно найти с помощью локатора fileHandle, пока поток не будет закрыт. Пользовательские агенты стараются гарантировать, что не произойдет частичная запись, т. е. файл будет либо содержать свое старое содержимое, либо все данные, которые были записаны через поток, до тех пор, пока поток не будет закрыт.
... создание активной (и дорогостоящей) копии содержимого файла (внутри createWritable( { keepExistingData: true } )
) сначала, прежде чем любая другая запись, это означает, что если FileSystemWritableFileStream
перемещается для перезаписи существующих данных и если большая операция в какой-то момент завершается неудачей ( поэтому FileSystemWritableFileStream.close()
никогда не вызывается), то файл всегда можно вернуть к исходному содержимому.
В то время как в обычной ОС и файловой системе, если вы открываете существующий файл для записи/добавления, а затем перемещаете записывающую головку в более старые данные, а затем перезаписываете достаточно существующих данных, тогда буферы ОС в конечном итоге будут сброшены на диск, оставив файл в неопределенное и необратимое состояние, если произойдет что-то плохое, прежде чем вы сможете правильно промыть и закрыть.
...хотя, по крайней мере, традиционные ОС/файловые системы предлагают какой-то «настоящий» режим добавления и/или способ явного выбора между Open-Existing и Truncate (O_APPEND
и O_TRUNC
в POSIX, TRUNCATE_EXISTING
в Windows, хотя в Windows вы нужно переместить головку потока самостоятельно).
Для записи 5 МБ в новый файл требуется ~22 мс, но также требуется ~23 мс для добавления одного байта.
Я чувствую, что у вашего теста есть небольшой недостаток: использование console.time
означает выполнение блокирующего синхронного вызова (точно так же, как console.info
). Итак, я взял ваш тест и заменил console.time
на performance.mark
, и я получил гораздо меньшие общие цифры - я также измеряю каждый шаг отдельно - и результаты показывают, что запись одного байта действительно происходит быстро - замедление appendByte
происходит из-за безопасного копирования. в await fileHandle.createWritable( { keepExistingData: true } )
:
Опубликовано в JSFiddle (поскольку у OPFS есть проблемы с фрагментами StackOverflow): https://jsfiddle.net/v6phgfba/
async function run() {
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('append.log', { create: true });
await create5MBFile(fileHandle); // ~13ms
await appendByte(fileHandle, { keepExistingData: true, mode: "exclusive" } ); // ~19ms
await appendByte(fileHandle, { mode: "exclusive" } ); // ~4ms (but doesn't actually append: the data is lost)
};
async function create5MBFile(fileHandle/*: FileSystemFileHandle*/) {
window.performance.mark('start');
const chunk = new Uint8Array(5 * 1024 * 1024).fill(0);
window.performance.mark('allocated 5MB');
const writable = await fileHandle.createWritable();
window.performance.mark('got writeable');
try {
await writable.write(chunk);
window.performance.mark('wrote 5MB');
}
finally {
await writable.close();
window.performance.mark('closed writeable');
const measurements = [
window.performance.measure( 'M1: allocation' , /*from:*/ 'start' , /*to:*/ 'allocated 5MB' ), // 1.5ms
window.performance.measure( 'M2: get writeable', /*from:*/ 'allocated 5MB', /*to:*/ 'got writeable' ), // 0.9ms
window.performance.measure( 'M3: write time' , /*from:*/ 'got writeable', /*to:*/ 'wrote 5MB' ), // 10.3ms
window.performance.measure( 'M4: close time' , /*from:*/ 'wrote 5MB' , /*to:*/ 'closed writeable' ) // 1.5ms
];
console.info( measurements );
console.info( "create5MBFile: total time: %f ms", measurements.reduce( ( a, m ) => a + m.duration, 0 ) );
}
};
async function appendByte(fileHandle/*: FileSystemFileHandle*/, writeableOpts) {
window.performance.mark('start');
const byteToAppend = new Uint8Array([0]);
window.performance.mark('allocated 1B');
const file = await fileHandle.getFile();
const currentSize = file.size;
window.performance.mark('got File/Blob length');
const writable = await fileHandle.createWritable(writeableOpts);
window.performance.mark('got writable');
try {
await writable.write({ type: 'write', position: currentSize, data: byteToAppend });
window.performance.mark('wrote 1B after-end');
}
finally {
await writable.close();
window.performance.mark('closed writeable');
const measurements = [
window.performance.measure( 'M1: allocation' , /*from:*/ 'start' , /*to:*/ 'allocated 1B' ), // 0ms
window.performance.measure( 'M2: get length' , /*from:*/ 'allocated 1B' , /*to:*/ 'got File/Blob length' ), // 0.2ms
window.performance.measure( 'M3: get writeable', /*from:*/ 'got File/Blob length', /*to:*/ 'got writable' ), // 17.4ms
window.performance.measure( 'M4: write time' , /*from:*/ 'got writable' , /*to:*/ 'wrote 1B after-end' ), // 0.9ms
window.performance.measure( 'M5: close time' , /*from:*/ 'wrote 1B after-end' , /*to:*/ 'closed writeable' ) // 1.7ms
];
console.info( measurements );
console.info( "appendByte: total time: %f ms", measurements.reduce( ( a, m ) => a + m.duration, 0 ) );
}
};
run();
Мои результаты (Chrome 127 x64 в Windows 10 22H2 x64) после запуска приведенного выше сценария 3 раза подряд:
[
{
duration : 1.5999999940395355,
entryType: "measure",
name : "M1: allocation",
startTime: 141.10000002384186
},
{
duration : 0.9000000059604645,
entryType: "measure",
name : "M2: get writeable",
startTime: 142.7000000178814
},
{
duration : 10.099999994039536,
entryType: "measure",
name : "M3: write time",
startTime: 143.60000002384186
},
{
duration : 1,
entryType: "measure",
name : "M4: close time",
startTime: 153.7000000178814
}
]
/* "create5MBFile: total time: 13.599ms */,
[
{
duration : 0,
entryType: "measure",
name : "M1: allocation",
startTime: 155.2000000178814
},
{
duration : 0.29999998211860657,
entryType: "measure",
name : "M2: get length",
startTime: 155.2000000178814
},
{
duration : 19.30000001192093,
entryType: "measure",
name : "M3: get writeable",
startTime: 155.5
},
{
duration : 5.0999999940395355,
entryType: "measure",
name : "M4: write time",
startTime: 174.80000001192093
},
{
duration : 1.300000011920929,
entryType: "measure",
name : "M5: close time",
startTime: 179.90000000596046
}
]
/*"appendByte: total time: 26ms */
[
{
duration : 0,
entryType: "measure",
name : "M1: allocation",
startTime: 181.40000000596046
},
{
duration : 0.4000000059604645,
entryType: "measure",
name : "M2: get length",
startTime: 181.40000000596046
},
{
duration : 0.9000000059604645,
entryType: "measure",
name : "M3: get writeable",
startTime: 181.80000001192093
},
{
duration : 0.699999988079071,
entryType: "measure",
name : "M4: write time",
startTime: 182.7000000178814
},
{
duration : 1.2000000178813934,
entryType: "measure",
name : "M5: close time",
startTime: 183.40000000596046
}
]
/*"appendByte: total time: 3.200ms", */
Есть ли лучший способ добавления данных?
inPlace
варианте, который устранит безопасное копирование (хотя, мне кажется, если бы у них был настоящий режим только добавления, такой, что старые данные не могли бы быть сохранены). тогда его нельзя будет перезаписать, это тоже будет так же безопасно, не правда ли?)
inPlace
было прекращено в 2019 году , хотя оно продолжается в другой теме здесь: https://github.com/WICG/file-system-access/issues/260Тем не менее, ~10 мс - это буквально доля мгновения - если «стоимость» безопасной (r) записи файла представляет собой неуловимую задержку при записи, то я полностью за.
Спасибо за расследование! Для меня ключевым моментом является то, что «замедление в AppendByte происходит из-за безопасного копирования в await fileHandle.createWritable({keepExistingData: true})
»
@NVI Я мог бы предложить вместо добавления файлов просто оставить существующие файлы как есть; вместо этого поддерживайте карту файлов, чтобы знать, какие файлы следует объединить в правильном порядке на случай, если вам понадобится сгенерировать сериализованный вывод.
Да, в настоящее время я изучаю подобные подходы, в которых файлы являются неизменяемыми, например. Таблица SSTable.
Почему бы не собрать все данные порциями (пакетной обработки) и записать их как одну операцию.