Как эффективно добавить в файл?

Я добавляю данные в файл журнала с помощью 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();

Почему бы не собрать все данные порциями (пакетной обработки) и записать их как одну операцию.

Derek Roberts 24.08.2024 22:33

Это обходной путь. Обратной стороной является то, что данные фактически не сохраняются до тех пор, пока не произойдет запись. Я выполняю пакетную обработку в реальном приложении (куски записываются, когда они достигают определенного размера или через 15 секунд после добавления первых данных), но я не хотел слишком усложнять вопрос.

NVI 25.08.2024 04:46

Во-первых: я не вижу разумных оснований ожидать схожих профилей производительности между средами выполнения — ни при сравнении ① (синхронные API с собственными привязками файловой системы) с ② (асинхронные API с привязками к абстракции виртуализированной файловой системы). Что касается того, что доступно для OPFS: существует метод FileSystemFileHandle: createSyncAccessHandle(), доступный только в рабочих контекстах — он обеспечивает преимущества в производительности за счет синхронного доступа на основе мьютекса.

jsejcksn 27.08.2024 19:12
^ Учитывая этот контекст: я не уверен, какой ответ вы на самом деле ищете — я не вижу в посте прямого, объективного вопроса. Можете ли вы отредактировать вопрос, чтобы уточнить? Возможно, вам нужен пример того, как использовать API, который я упомянул… если да, то я или кто-то другой может написать его в качестве ответа, но ни я, ни кто-либо здесь не может предложить вам гарантии производительности.
jsejcksn 27.08.2024 19:15

Я реализовал свой собственный тест, и у меня было около 1,5 мс на запись/добавление 1 байта. Я думаю, проблема в том, что вы используете console.time, который включает синхронный вызов console.info, который медленный. Вместо этого мой использует window.performance.mark.

Dai 27.08.2024 21:32

Похоже, keepExistingData: true — это дорогая часть — WHATWG знает, что это не подходит для более серьёзного ввода-вывода файлов, поэтому говорят о новой опции: inPlace, которая должна ускорять добавление, потому что нет копии.

Dai 27.08.2024 21:38
^ @Dai API синхронизации, о котором я упоминал ранее, предназначен именно для этого — информация об этом есть в конце примечания в спецификации, на которую вы ссылались.
jsejcksn 27.08.2024 22:07
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
7
122
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я не сохранял ничего вроде флага добавления. Мне пришлось прочитать размер файла и начать писать с этой позиции, чтобы добавить данные. Есть ли лучший способ добавления данных?

Текущий дизайн метода 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",  */


Есть ли лучший способ добавления данных?


Тем не менее, ~10 мс - это буквально доля мгновения - если «стоимость» безопасной (r) записи файла представляет собой неуловимую задержку при записи, то я полностью за.

Спасибо за расследование! Для меня ключевым моментом является то, что «замедление в AppendByte происходит из-за безопасного копирования в await fileHandle.createWritable({keepExistingData: true})»

NVI 28.08.2024 00:21

@NVI Я мог бы предложить вместо добавления файлов просто оставить существующие файлы как есть; вместо этого поддерживайте карту файлов, чтобы знать, какие файлы следует объединить в правильном порядке на случай, если вам понадобится сгенерировать сериализованный вывод.

Dai 28.08.2024 00:23

Да, в настоящее время я изучаю подобные подходы, в которых файлы являются неизменяемыми, например. Таблица SSTable.

NVI 28.08.2024 00:36

Другие вопросы по теме