Перейти к содержанию

Числовые массивы и стоимость передачи данных

Пять агрегатов «Меры» показывают общую структуру текста, но не распределение длины строк. Добавим гистограмму: сколько пустых строк, сколько коротких и сколько длинных. При этом не будем передавать в JavaScript список каждой строки. Для диаграммы достаточно нескольких счётчиков, а передача большого промежуточного массива увеличила бы стоимость обработки.

Урок посвящён двум связанным решениям: вычислять компактный результат рядом с исходными данными и понимать, кому принадлежит числовой буфер после перехода границы Wasm. Автоматическая обвязка облегчает работу, но не отменяет копирование и правила памяти.

Фиксированные интервалы

Зададим шесть интервалов длины в Unicode-скалярных значениях: пустая строка, 1–20, 21–40, 41–80, 81–120 и больше 120. Подписи находятся в JavaScript, а Rust возвращает числа в фиксированном порядке. Это часть той же версии договора, что и статистика.

Добавим в src/lib.rs полный экспорт. Проверка лимита остаётся общей, а правило удаления завершающего CR повторяет договор предыдущих уроков:

#[wasm_bindgen]
pub fn line_histogram(text: &str) -> Result<Vec<u32>, JsError> {
    analyze_checked(text).map_err(JsError::new)?;
    let mut buckets = vec![0u32; 6];
    if text.is_empty() {
        return Ok(buckets);
    }
    for raw in text.split('\n') {
        let row = raw.strip_suffix('\r').unwrap_or(raw);
        let length = row.chars().count();
        let bucket = match length {
            0 => 0,
            1..=20 => 1,
            21..=40 => 2,
            41..=80 => 3,
            81..=120 => 4,
            _ => 5,
        };
        buckets[bucket] += 1;
    }
    Ok(buckets)
}

Для кот\nhi ожидается [0, 2, 0, 0, 0, 0]. Если добавить завершающий LF, первая корзина увеличится на один. Сумма корзин должна соответствовать общему числу строк по нашему договору. Это полезная смысловая связь, но не выдуманный отчёт выполненной проверки.

Пустая строка отличается от строки из пробелов. Последняя имеет ненулевую техническую длину и попадёт в соответствующий интервал, даже если не считается непустой для другого показателя. Мы не применяем trim перед гистограммой: иначе изменение исходных данных оказалось бы скрытым внутри визуализации.

Почему не возвращаем длину каждой строки

Файл из одних LF может содержать миллионы строк даже при лимите восемь MiB. Массив u32 для каждой длины занимает несколько байт на запись и может быть намного больше исходного текста. Затем его придётся передать в JavaScript и обработать ещё раз для получения всего шести счётчиков.

Компактная гистограмма ограничивает размер результата независимо от числа строк. Такой приём применим и за пределами Wasm: сервер, Worker или база данных могут вычислять агрегат рядом с данными. Прежде чем оптимизировать копирование, полезно решить, нужны ли сами промежуточные данные вызывающей стороне.

Если интерфейс должен показывать таблицу отдельных строк, договор будет другим. Можно передавать выбранный диапазон или страницы результата. Но не следует загружать все строки ради диаграммы, у которой только шесть столбцов. Изменение требования должно быть явным, а не спрятанным в универсальной функции «на будущее».

Типизированный массив на стороне JavaScript

Для экспортируемого Vec<u32> wasm-bindgen предоставляет согласованную обвязку типизированного результата. В нашем адаптере сразу проверим ожидаемую длину и преобразуем данные в обычный компактный массив для сообщения Worker и интерфейса:

function histogram(module, text) {
  const values = module.line_histogram(text);
  if (values.length !== 6) throw new Error("Unexpected histogram ABI");
  return Array.from(values);
}

Мы не обращаемся самостоятельно к указателю Rust и не освобождаем память через угаданный экспорт. Сгенерированный код знает форму возврата, получает данные и управляет соответствующей границей владения. Точные свойства преобразований следует сверять с руководством wasm-bindgen по поддерживаемым типам.

Типизированный массив не означает, что JavaScript получил произвольный вечный вид на внутреннюю память модуля. Для возвращаемого владеющего вектора обвязка может копировать данные в отдельный результат и освобождать Rust-выделение. Поэтому не заявляйте «нулевое копирование» только из-за появления Uint32Array.

Если работать с линейной памятью вручную

В более низкоуровневой реализации можно получить WebAssembly.Memory и построить представление над её buffer. Тогда разработчик отвечает за время жизни указателя, размер области и последствия роста памяти. Старое представление может перестать соответствовать актуальному буферу после изменения памяти.

Для шести счётчиков такая сложность не оправдана. Мы остаёмся на обычной экспортируемой границе. Это не ограничивает изучение Wasm: напротив, позволяет сначала понять модель и лишь затем вводить ручное управление там, где оно решает измеренную проблему.

Передача исходной JavaScript-строки тоже имеет цену: данные должны попасть в представление, доступное Rust. Если вызвать analyze_text, а затем line_histogram, текст может передаваться дважды. В исходном учебном варианте это сделано для ясности. Для рабочего продукта можно объединить операции в один экспорт с общим проходом и одиннадцатью возвращаемыми числами.

Согласованное объединение результатов

Адаптер возвращает именованный объект статистики и массив histogram. Подписи интервалов зададим рядом с выводом: 0, 1–20, 21–40, 41–80, 81–120, 121+. Не переводим технические границы в локализованную строку внутри Rust; язык интерфейса не меняет алгоритм распределения.

Для большого текста будем оценивать всю операцию: чтение файла, декодирование, передачу, вычисление двух экспортов и отображение. Измерение только второго прохода после прогрева не отвечает на вопрос, быстрее ли приложение для человека. Поздний урок о сравнении движков разложит эту задачу на наблюдаемые интервалы.

Изменение интервалов является изменением договора

Предположим, дизайнер хочет заменить границу 20 на 30. Это не только изменение подписи столбца: Rust и JavaScript должны распределять строки по новым правилам. Если поменять лишь легенду, диаграмма станет убедительно показывать неверные категории. Границы нужно сохранить как часть версии API или явно передавать в обе реализации с проверкой допустимых значений.

Для учебной серии интервалы фиксированы и одинаковы во всех исходниках. Объект результата не хранит исходные строки, поэтому он мал и удобен для передачи. Однако вместе с массивом чисел интерфейс обязан знать порядок категорий. Уточнение названия «121+» на «Больше 120» сохраняет смысл; перестановка категорий — уже нет.

Если понадобится произвольная гистограмма, сначала задайте максимальное число корзин и политику крайних значений. Без ограничений пользовательский ввод может потребовать огромный массив результата и уничтожить преимущество компактного агрегата. Так предметный договор снова оказывается важнее конкретного низкоуровневого типа.

Теперь результат остаётся маленьким и имеет понятный порядок. Следующий шаг — перенести вычисление в Web Worker. Там появятся асинхронные сообщения, идентификатор операции и проблема устаревшего ответа, которую нельзя решить одним массивом чисел.

Оглавление серии