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

Запасная реализация с тем же контрактом

Вычисление «Меры» работает через Worker, но пакет Wasm может оказаться недоступным: ресурс не загрузился, путь не совпал или браузерная среда не поддерживает нужный механизм. Для нашей небольшой статистики уже есть JavaScript-реализация. Сделаем её запасным движком, сохранив точный договор результата.

Запасная реализация полезна только тогда, когда выполняет ту же задачу. Если Rust считает скалярные значения, а JavaScript — кодовые единицы UTF-16, пользователи получат разные числа для одного файла. Поэтому сначала согласуем семантику, а потом подключим выбор движка.

Полная запасная операция

Обновим web/fallback.mjs: добавим ограничение входа и гистограмму. Ниже полный вычислительный блок. Он принимает корректную строку, уже полученную строгим декодированием файла:

const MAX_BYTES = 8 * 1024 * 1024;

export function analyzeText(text) {
  const bytes = new TextEncoder().encode(text).length;
  if (bytes > MAX_BYTES) throw new Error("Text exceeds the 8 MiB limit");
  const rows = text === "" ? [] : text.split("\n").map(row =>
    row.endsWith("\r") ? row.slice(0, -1) : row);
  const histogram = Array(6).fill(0);
  let nonEmptyLines = 0;
  let maxLineScalars = 0;
  for (const row of rows) {
    const length = [...row].length;
    if (/[^ \t]/u.test(row)) nonEmptyLines += 1;
    maxLineScalars = Math.max(maxLineScalars, length);
    const index = length === 0 ? 0 : length <= 20 ? 1 :
      length <= 40 ? 2 : length <= 80 ? 3 : length <= 120 ? 4 : 5;
    histogram[index] += 1;
  }
  return {
    bytes, scalars: [...text].length, lines: rows.length,
    nonEmptyLines, maxLineScalars, histogram
  };
}

Здесь повторяются именно правила предметной задачи: LF, удаление одного завершающего CR, отдельная оценка непустоты и фиксированные интервалы. Повтор не спрятан в другой смысл методов lines или trim. Для контрольного кот\nhi статистика и гистограмма должны совпасть с Rust.

JavaScript допускает строку с непарным суррогатом, тогда как Rust &str представляет корректный UTF-8. В пользовательском пути такие значения не появляются: TextDecoder со строгой ошибкой получает валидный файл. Если экспортировать функцию для произвольных строк другого приложения, адаптер должен отдельно отклонять непарные суррогаты или явно нормализовать их одинаково для обоих движков.

Загрузка с явным выбором

Изменим loadEngine в web/engine.mjs. Сначала пробуем Wasm, при ошибке инициализации выбираем JavaScript. Сам успешный вызов вычисления не оборачиваем автоматическим повтором через другой движок: предметные ошибки должны оставаться ошибками.

import { analyzeText } from "./fallback.mjs";

async function loadEngine() {
  try {
    const module = await import("./pkg/mera.js");
    await module.default();
    return {
      name: "WebAssembly",
      analyze(text) {
        const values = module.analyze_text(text);
        const histogram = module.line_histogram(text);
        if (values.length !== 5 || histogram.length !== 6) {
          throw new Error("Unexpected module ABI");
        }
        const [bytes, scalars, lines, nonEmptyLines, maxLineScalars] = values;
        return { bytes, scalars, lines, nonEmptyLines, maxLineScalars,
          histogram: Array.from(histogram) };
      }
    };
  } catch (error) {
    return {
      name: "JavaScript",
      reason: error instanceof Error ? error.message : String(error),
      analyze: analyzeText
    };
  }
}

Ошибка отсутствующего файла происходит при подготовке движка, поэтому запасной путь уместен. Слишком большой текст или неправильный внешний результат происходит после выбора. Если повторять каждый такой отказ скрыто через JavaScript, можно замаскировать несовместимость API или случайно обработать вход по другим правилам.

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

Запасной движок также выполняется в Worker

Загрузчик вызывается внутри Worker, поэтому JavaScript-функция тоже не занимает основной поток страницы. Это помогает разделить два вопроса: язык вычисления и место выполнения. Пользовательская отзывчивость может улучшиться от переноса задачи в Worker, даже если сам расчёт остаётся на JavaScript.

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

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

Сравнение договора без красивых замеров

До сравнения скорости нужно подтвердить одинаковый результат на характерных входах: пустой файл, завершающий LF, CRLF, русский текст, emoji и строки из пробелов. Добавим в будущую техническую приёмку проверку всех пяти полей и шести корзин. Сейчас такой запуск запрещён условиями работы и не проводился.

Если результаты различаются, сначала ищем смысловую причину. Например, text.length даст другое число для emoji, а trim() может удалить пробелы, которые наш договор сохраняет в длине. Более быстрый неправильный результат не является оптимизацией алгоритма.

Можно также посмотреть на непарный суррогат в искусственной JavaScript-строке. Это важный случай для общего API, но не нужно выдавать его за содержимое корректного UTF-8-файла. Предпосылки вызывающего кода определяют, какие входы действительно допустимы.

Как расширять реализацию

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

Принудительный режим для технической приёмки

Обычный пользователь получает автоматический выбор, но разработчику нужно отдельно посмотреть каждую ветвь. В итоговом загрузчике предусмотрен параметр режима auto, javascript или wasm. Первый разрешает запасной путь; второй сразу выбирает JavaScript; третий возвращает ошибку, если Wasm подготовить не удалось. Принудительный режим не должен незаметно переключаться на другую реализацию.

Такой параметр полезен и при диагностике ресурса. Если браузер показал правильные числа в автоматическом режиме, это ещё не доказывает доступность бинарника: могла сработать запасная функция. Отдельное сообщение движка и явный выбор позволяют понять причину без догадок. Сохранённый отчёт должен указывать фактическую ветвь, а не название, выбранное в исходной форме.

Для одинаковых входов сравниваются обычные числа и категории. Разные объекты памяти или тип массива сами по себе не являются расхождением смысла, если адаптер уже привёл их к договору. Напротив, одинаковая длина результата не доказывает, что порядок полей верен. Нужно рассматривать именованные показатели после преобразования.

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

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

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

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