Пулы объектов

Проблема частого создания объектов в картографических приложениях

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

Каждое создание маркера или полигона в HERE Maps API включает:

  • выделение памяти под объект JavaScript;
  • создание связанного DOM-слоя или WebGL-объекта;
  • привязку к внутренним структурам карты;
  • регистрацию событий (hover, click, drag);
  • добавление в слой рендеринга.

Удаление объекта, в свою очередь, требует:

  • разрыва связей с картой;
  • очистки событий;
  • освобождения ресурсов рендера;
  • потенциального пересчёта слоя.

При интенсивных обновлениях (например, трекинг транспорта, визуализация IoT-данных, движение объектов в реальном времени) постоянное создание и уничтожение объектов приводит к:

  • фрагментации памяти;
  • увеличению нагрузки на garbage collector;
  • микрофризам интерфейса;
  • деградации FPS при анимации карты.

Эта проблема решается применением паттерна пул объектов.


Суть пула объектов в картографическом контексте

Пул объектов — это структура, в которой заранее создаётся ограниченное количество экземпляров тяжёлых объектов (маркер, полилиния, информационное окно), после чего они переиспользуются вместо постоянного пересоздания.

В контексте карт:

  • объект создаётся один раз;
  • при необходимости скрывается или переиспользуется;
  • изменяются только его свойства (позиция, стиль, данные);
  • возврат в пул означает отключение от карты без уничтожения.

Ключевая идея: стоимость создания заменяется стоимостью конфигурации.


Базовая архитектура пула маркеров

Наиболее распространённый сценарий — пул маркеров. В HERE Maps API маркеры являются относительно тяжёлыми объектами, особенно при использовании кастомных иконок и HTML-слоёв.

Структура пула обычно включает:

  • массив доступных объектов;
  • массив занятых объектов;
  • методы acquire и release.
class MarkerPool {
  constructor(map, factory, size = 100) {
    this.map = map;
    this.factory = factory;
    this.pool = [];
    this.inUse = new Set();

    for (let i = 0; i < size; i++) {
      const marker = this.factory();
      marker.setVisibility(false);
      this.pool.push(marker);
    }
  }

  acquire() {
    const marker = this.pool.pop();
    if (!marker) return null;

    this.inUse.add(marker);
    marker.setVisibility(true);
    return marker;
  }

  release(marker) {
    marker.setVisibility(false);
    this.inUse.delete(marker);
    this.pool.push(marker);
  }
}

Фабрика маркера может выглядеть так:

function createMarker(map) {
  const marker = new H.map.Marker({ lat: 0, lng: 0 });
  map.addObject(marker);
  marker.setVisibility(false);
  return marker;
}

Переиспользование маркеров вместо удаления

Классическая ошибка — удаление объекта с карты:

map.removeObject(marker);

В модели пула такой подход заменяется:

pool.release(marker);

Объект не уничтожается, а:

  • скрывается;
  • очищает данные;
  • сбрасывает состояние;
  • возвращается в пул.
release(marker) {
  marker.setGeometry({ lat: 0, lng: 0 });
  marker.setData(null);
  marker.setVisibility(false);
  this.inUse.delete(marker);
  this.pool.push(marker);
}

Динамическое обновление данных через пул

В системах реального времени (например, GPS-трекинг) важно обновлять только геометрию объекта.

function updateVehicle(pool, id, coords) {
  let marker = pool.inUse.get(id);

  if (!marker) {
    marker = pool.acquire();
    pool.inUse.set(id, marker);
  }

  marker.setGeometry(coords);
}

Здесь добавляется дополнительный слой — индексирование активных объектов по идентификатору.


Пул полилиний и маршрутов

Полилинии (routes, trajectories) значительно тяжелее маркеров, так как содержат массив координат и требуют пересчёта геометрии.

Пул для линий должен учитывать:

  • очистку массива точек;
  • сброс стиля;
  • отключение от событий.
class PolylinePool {
  constructor(map, size = 20) {
    this.map = map;
    this.pool = [];

    for (let i = 0; i < size; i++) {
      const line = new H.map.Polyline([], {
        style: { strokeColor: 'blue', lineWidth: 3 }
      });

      map.addObject(line);
      line.setVisibility(false);

      this.pool.push(line);
    }
  }

  acquire() {
    const line = this.pool.pop();
    if (!line) return null;

    line.setVisibility(true);
    return line;
  }

  release(line) {
    line.setGeometry([]);
    line.setVisibility(false);
    this.pool.push(line);
  }
}

Работа с оверлеями через пул

Оверлеи (HTML overlays, bubble windows) являются наиболее дорогими объектами из-за DOM.

Проблемы:

  • постоянное создание DOM-узлов;
  • перерасчёт layout;
  • привязка событий;
  • утечки памяти при неправильном удалении.

Пул позволяет держать фиксированное количество окон.

class InfoBubblePool {
  constructor(map, size = 10) {
    this.map = map;
    this.pool = [];

    for (let i = 0; i < size; i++) {
      const bubble = new H.ui.InfoBubble({ lat: 0, lng: 0 }, {
        content: ''
      });

      bubble.close();
      this.pool.push(bubble);
    }
  }

  acquire() {
    return this.pool.pop() || null;
  }

  release(bubble) {
    bubble.setContent('');
    bubble.close();
    this.pool.push(bubble);
  }
}

Оптимизация через lazy reset состояния

Часто сброс всех свойств объекта при возврате в пул приводит к лишней нагрузке. Более эффективный подход — ленивый сброс.

Принцип:

  • сбрасываются только критические поля;
  • вторичные значения обновляются при повторном использовании.
release(marker) {
  marker._id = null;
  marker.setVisibility(false);
  this.pool.push(marker);
}

При повторном использовании все значения перезаписываются полностью.


Иерархия пулов в больших системах

В сложных приложениях используется несколько уровней пулов:

  • глобальный пул маркеров;
  • пул на слой (traffic, vehicles, POI);
  • локальный пул в модуле (например, кластеризация).

Пример архитектуры:

Map
 ├── TrafficLayerPool
 │     ├── VehicleMarkerPool
 │     ├── IncidentMarkerPool
 │
 ├── POILayerPool
 │     ├── RestaurantMarkerPool
 │     ├── HotelMarkerPool
 │
 └── RouteLayerPool
       ├── PolylinePool
       ├── ArrowOverlayPool

Такой подход позволяет:

  • изолировать нагрузки;
  • управлять лимитами ресурсов;
  • уменьшать конкуренцию за память.

Пулы и кластеризация объектов

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

Алгоритм:

  • объекты группируются;
  • создаются кластеры;
  • маркеры пула используются только для отображения кластеров.
function renderCluster(cluster, pool) {
  const marker = pool.acquire();

  marker.setGeometry(cluster.center);
  marker.setData({
    count: cluster.points.length
  });

  return marker;
}

Это снижает количество активных объектов в разы.


Управление переполнением пула

Ситуация, когда пул исчерпан, требует стратегии:

  1. возврат null;
  2. создание временного объекта;
  3. расширение пула;
  4. вытеснение старых объектов.

Наиболее стабильная стратегия — ограниченное расширение:

acquire() {
  if (this.pool.length === 0) {
    this.expand(10);
  }

  return this.pool.pop();
}

Типичные ошибки при реализации пулов

Распространённые проблемы:

  • повторное добавление объекта на карту без удаления старой ссылки;
  • отсутствие очистки событий;
  • утечки через замыкания;
  • хранение состояния вне объекта пула;
  • несинхронизированное состояние между inUse и pool.

Особенно критична ошибка двойного добавления:

map.addObject(marker);
map.addObject(marker); // приводит к дублированию слоёв

Производительность и влияние на рендеринг карты

Использование пулов влияет на:

  • стабильность FPS при анимации;
  • скорость обновления объектов;
  • нагрузку на garbage collector;
  • время отклика интерфейса при массовых изменениях.

Наиболее заметный эффект наблюдается при сценариях:

  • движение 500+ объектов одновременно;
  • обновление координат каждые 100–200 мс;
  • динамическая фильтрация данных на карте.

Пул позволяет заменить модель:

create → render → destroy

на:

acquire → update → release → reuse

Комбинирование пулов с потоковой обработкой данных

При интеграции с WebSocket или потоками данных:

  • данные поступают непрерывно;
  • объекты не пересоздаются;
  • пул служит буфером визуализации.
socket.onmess age = (event) => {
  const data = JSON.parse(event.data);

  const marker = pool.acquire();
  if (!marker) return;

  marker.setGeometry(data.coords);
  marker.setData(data);
};

Такой подход предотвращает деградацию производительности при всплесках данных.


Состояния объектов внутри пула

Объекты внутри пула могут находиться в нескольких состояниях:

  • свободен (idle);
  • активен (in use);
  • ожидает сброса;
  • в процессе обновления.

Чёткое управление состояниями предотвращает:

  • гонки данных;
  • визуальные артефакты;
  • утечки памяти.
const STATE = {
  IDLE: 0,
  ACTIVE: 1,
  RESETTING: 2
};

Поведение пула при долгоживущих приложениях

В приложениях, работающих часами или днями:

  • пул стабилизирует потребление памяти;
  • снижает вероятность деградации GC;
  • предотвращает накопление DOM-узлов.

Однако требуется периодическая ревизия:

  • очистка неиспользуемых объектов;
  • пересоздание пула при деградации;
  • контроль утечек ссылок.

Связь пулов с архитектурой слоёв карты

Пулы естественно интегрируются в слоистую архитектуру HERE Maps:

  • базовый слой тайлов;
  • слой данных (объекты из пула);
  • интерактивный слой (события и UI).

Такое разделение снижает связность и упрощает управление жизненным циклом объектов, которые отображаются на карте.