alignSeries (v2)

Revision 2 of this benchmark created on


Setup

const asc = (a, b) => a - b;

const alignSeries = (series) => {
  const timestampsSet = new Set([...series.flatMap((item) => item.map((v) => v[0] / 1000))]);
  const timestamps = Array.from(timestampsSet).sort(asc);

  const seriesMaps = series.map((s) => new Map(s));
  const alignedSeries = series.map(() => new Array(timestamps.length).fill(null));

  // the scaleFactors array stores a scaling factor for each series
  // to ensure that extremely large values (> Number.MAX_SAFE_INTEGER)
  // are scaled down to avoid potential issues with js number precision limits
  const scaleFactors = [];

  for (let j = 0; j < series.length; j++) {
    let maxSeriesValue = Number.NEGATIVE_INFINITY;

    for (let i = 0; i < timestamps.length; i++) {
      const ts = timestamps[i];
      const v = seriesMaps[j].get(ts * 1000);

      alignedSeries[j][i] = v ?? null;

      maxSeriesValue = v && maxSeriesValue < v ? v : maxSeriesValue;
    }

    scaleFactors.push(maxSeriesValue > Number.MAX_SAFE_INTEGER ? 1e12 : 1);
  }

  return {
    scaleFactors,
    series: alignedSeries,
    timestamps,
  };
};

const alignSeriesAi = (series) => {
  // Early return for empty input
  if (!series.length) {
    return { scaleFactors: [], series: [], timestamps: [] };
  }

  // Single pass to collect timestamps and find max values
  const timestampSet = new Set();
  const maxValues = new Array(series.length).fill(Number.NEGATIVE_INFINITY);
  
  for (let j = 0; j < series.length; j++) {
    const s = series[j];
    if (!s.length) continue;

    for (const point of s) {
      const ts = point[0] / 1000;
      const value = point[1];
      
      timestampSet.add(ts);
      if (value > maxValues[j]) {
        maxValues[j] = value;
      }
    }
  }

  // Convert Set to sorted array
  const timestamps = Array.from(timestampSet).sort((a, b) => a - b);
  
  // Pre-allocate arrays
  const alignedSeries = series.map(() => new Array(timestamps.length).fill(null));
  const scaleFactors = new Array(series.length);

  // Create a single Map for each series to avoid repeated lookups
  const seriesMaps = series.map(s => {
    const map = new Map();
    for (const point of s) {
      map.set(point[0] / 1000, point[1]);
    }
    return map;
  });

  // Fill in values using the maps
  for (let j = 0; j < series.length; j++) {
    const seriesMap = seriesMaps[j];
    
    for (let i = 0; i < timestamps.length; i++) {
      alignedSeries[j][i] = seriesMap.get(timestamps[i]) ?? null;
    }
    
    scaleFactors[j] = maxValues[j] > Number.MAX_SAFE_INTEGER ? 1e12 : 1;
  }

  return {
    scaleFactors,
    series: alignedSeries,
    timestamps,
  };
};

const alignSeriesAi2 = (series) => {
  if (series.length === 0) {
    return { scaleFactors: [], series: [], timestamps: [] };
  }

  // Pre-allocate arrays with known sizes
  const timestamps = new Set();
  const seriesMaps = new Array(series.length);
  const alignedSeries = new Array(series.length);
  const scaleFactors = new Array(series.length);
  
  // Single pass to collect timestamps and create maps
  for (let j = 0; j < series.length; j++) {
    const currentSeries = series[j];
    const seriesMap = new Map();
    let maxSeriesValue = Number.NEGATIVE_INFINITY;
    
    for (let i = 0; i < currentSeries.length; i++) {
      const [ts, value] = currentSeries[i];
      const normalizedTs = Math.floor(ts / 1000);
      timestamps.add(normalizedTs);
      seriesMap.set(normalizedTs, value);
      
      if (value > maxSeriesValue) {
        maxSeriesValue = value;
      }
    }
    
    seriesMaps[j] = seriesMap;
    scaleFactors[j] = maxSeriesValue > Number.MAX_SAFE_INTEGER ? 1e12 : 1;
  }

  // Convert Set to sorted array once
  const sortedTimestamps = Array.from(timestamps).sort((a, b) => a - b);
  const timestampsLength = sortedTimestamps.length;

  // Pre-allocate aligned series arrays
  for (let j = 0; j < series.length; j++) {
    alignedSeries[j] = new Array(timestampsLength).fill(null);
    
    // Single pass through timestamps
    for (let i = 0; i < timestampsLength; i++) {
      alignedSeries[j][i] = seriesMaps[j].get(sortedTimestamps[i]) ?? null;
    }
  }

  return {
    scaleFactors,
    series: alignedSeries,
    timestamps: sortedTimestamps,
  };
};

const alignSeriesNew = (series, options) => {
  if (!series.length) {
    return { scaleFactors: [], series: [], timestamps: [] };
  }

  const { since, until } = options || {};
  const totalPoints = series.reduce((sum, s) => sum + s.length, 0);
  const isZoomedIn = since != null && until != null && until - since < ONE_DAY_MS;
  const needsSampling = totalPoints > 50000 && !isZoomedIn;

  const timestampMap = new Map(); // timestamp -> index map
  const allTimestamps = [];

  for (const s of series) {
    const len = s.length;

    if (!needsSampling) {
      for (let i = 0; i < len; i++) {
        const ts = s[i][0] / 1000;

        if (!timestampMap.has(ts)) {
          timestampMap.set(ts, allTimestamps.length);
          allTimestamps.push(ts);
        }
      }

      continue;
    }

    const windowSize = Math.max(10, Math.floor(len / 10000)); // aim for ~10k points

    for (let i = 0; i < len; i += windowSize) {
      const end = Math.min(i + windowSize, len);
      let minPoint = s[i];
      let maxPoint = s[i];

      const first = s[i];
      const firstTs = first[0] / 1000;
      if (!timestampMap.has(firstTs)) {
        timestampMap.set(firstTs, allTimestamps.length);
        allTimestamps.push(firstTs);
      }

      for (let k = i; k < end; k++) {
        const point = s[k];
        if (point[1] < minPoint[1]) {
          minPoint = point;
        }
        if (point[1] > maxPoint[1]) {
          maxPoint = point;
        }
      }

      const minTs = minPoint[0] / 1000;
      if (!timestampMap.has(minTs)) {
        timestampMap.set(minTs, allTimestamps.length);
        allTimestamps.push(minTs);
      }

      const maxTs = maxPoint[0] / 1000;
      if (!timestampMap.has(maxTs)) {
        timestampMap.set(maxTs, allTimestamps.length);
        allTimestamps.push(maxTs);
      }
    }
  }

  allTimestamps.sort((a, b) => a - b);
  for (let i = 0; i < allTimestamps.length; i++) {
    timestampMap.set(allTimestamps[i], i);
  }

  const alignedSeries = new Array(series.length);

  // the scaleFactors array stores a scaling factor for each series
  // to ensure that extremely large values (> Number.MAX_SAFE_INTEGER)
  // are scaled down to avoid potential issues with js number precision limits
  const scaleFactors = new Array(series.length);

  for (let j = 0; j < series.length; j++) {
    const aligned = new Array(allTimestamps.length).fill(null);
    let maxVal = Number.NEGATIVE_INFINITY;

    for (let k = 0; k < series[j].length; k++) {
      const [tsMs, val] = series[j][k];
      const ts = tsMs / 1000;
      const idx = timestampMap.get(ts);
      if (idx !== undefined) {
        aligned[idx] = val ?? null;

        if (val !== null && val > maxVal) {
          maxVal = val;
        }
      }
    }

    alignedSeries[j] = aligned;
    scaleFactors[j] = maxVal > Number.MAX_SAFE_INTEGER ? 1e12 : 1;
  }

  return {
    scaleFactors,
    series: alignedSeries,
    timestamps: allTimestamps,
  };
};

const generateLargeDataSeries = (length) => {
      const series1 = [];
      const series2 = [];
      const series3 = [];

      // generate points 1 hour apart
      for (let i = 0; i < length; i++) {
        series1.push([1609459200000 + i * 3600 * 1000, i]);
        series2.push([1609459200000 + i * 3600 * 1000, i * 2]);
        series3.push([1609453200000 + i * 3600 * 1000, i * 3]);
      }

      return [series1, series2, series3];
}
    
const data = generateLargeDataSeries(200000)

Test runner

Ready to run.

Testing in
TestOps/sec
normal
alignSeries(data);
ready
ai
alignSeriesAi(data);
ready
ai2
alignSeriesAi2(data);
ready
new
alignSeriesNew(data);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.