Lattice Grid

blog

Histogram of API Response Times

Building…
Loading a live grid…

If you are the SRE on call for an API, the question is never what the average response time is. It is how slow the slowest requests get, and how many people meet them. An average of 90 milliseconds can sit comfortably on top of a handful of requests that take a full second, and the customers who hit those are the ones who write in.

A histogram answers that by counting. It sorts every request into a bucket of response times and draws one bar per bucket, so the pile of fast requests shows on the left and the long tail trails off to the right. The chart above is bound to the grid beneath it, one row per request, so filtering the rows redraws the chart. Filter the Endpoint column to /orders and its slow requests are all that is left to read; switch to /login and compare the tails.

Why a histogram and not an average

An average, or a bar chart of averages per endpoint, folds every request into one number and the slow ones vanish into it. A histogram keeps all of them, so a tail that is one request in thirty is still a bar you can see.

The chart over a grid

The chart reads the grid’s rows, so there is nothing to keep in step. A histogram takes one column, y, and counts how many rows fall in each bucket. Here is the example in plain JavaScript, with a dozen inline requests standing in for your own rows.

import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';

const rows = [
  { id: 1, endpoint: '/search', ms: 42 }, { id: 2, endpoint: '/search', ms: 51 },
  { id: 3, endpoint: '/search', ms: 58 }, { id: 4, endpoint: '/search', ms: 240 },
  { id: 5, endpoint: '/orders', ms: 95 }, { id: 6, endpoint: '/orders', ms: 118 },
  { id: 7, endpoint: '/orders', ms: 142 }, { id: 8, endpoint: '/orders', ms: 960 },
  { id: 9, endpoint: '/login', ms: 32 }, { id: 10, endpoint: '/login', ms: 38 },
  { id: 11, endpoint: '/login', ms: 45 }, { id: 12, endpoint: '/login', ms: 320 },
];

const gridEl = document.createElement('div');
const chartEl = document.createElement('div');
gridEl.style.height = '260px';
chartEl.style.height = '320px';
document.body.append(gridEl, chartEl);

const grid = createGrid(gridEl, {
  rowKey: 'id',
  rows,
  columns: [
    { field: 'endpoint', title: 'Endpoint', filter: { type: 'set' } },
    { field: 'ms', title: 'Response (ms)', type: 'number' },
  ],
});

createChart({ grid, container: chartEl, type: 'histogram', y: 'ms', buckets: 12,
  title: 'Response times in milliseconds' });

The same thing in React

The React adapter is a factory, so you build the component once at module scope. The ref’s grid is the real grid, which is what the chart binds to.

import React, { useEffect, useRef } from 'react';
import { createGrid } from '@toclocoinc/lattice-grid';
import '@toclocoinc/lattice-grid/css';
import { createLatticeGrid } from '@toclocoinc/lattice-grid/modules/react';
import { createChart } from '@toclocoinc/lattice-grid/modules/charts';

const LatticeGrid = createLatticeGrid({ React, createGrid });
const rows = [
  { id: 1, endpoint: '/search', ms: 42 }, { id: 2, endpoint: '/search', ms: 58 },
  { id: 3, endpoint: '/search', ms: 240 }, { id: 4, endpoint: '/orders', ms: 95 },
  { id: 5, endpoint: '/orders', ms: 142 }, { id: 6, endpoint: '/orders', ms: 960 },
  { id: 7, endpoint: '/login', ms: 32 }, { id: 8, endpoint: '/login', ms: 45 },
];
const columns = [
  { field: 'endpoint', title: 'Endpoint', filter: { type: 'set' } },
  { field: 'ms', title: 'Response (ms)', type: 'number' },
];

export function ResponseTimes() {
  const gridRef = useRef(null);
  const chartRef = useRef(null);
  useEffect(() => {
    const chart = createChart({ grid: gridRef.current.grid, container: chartRef.current,
      type: 'histogram', y: 'ms', buckets: 12, title: 'Response times in milliseconds' });
    return () => chart.destroy();
  }, []);
  return (<>
    <LatticeGrid ref={gridRef} rowKey="id" columns={columns} rows={rows} style={{ height: 220 }} />
    <div ref={chartRef} style={{ height: 280 }} />
  </>);
}

The options used

These are the rows of the charts reference for the options in the listings above.

Option Type What it does
grid Grid Required. The grid to read.
container Element or string Required. Where to draw.
type string 'histogram' here.
y string The measure column whose values are counted into buckets (the response time). A histogram takes y alone.
buckets number Histogram bins. Twelve by default.
title string Drawn above the plot.

Choosing the buckets

Twelve buckets is the default, and it is a fair start. Too few and the tail is swallowed into the last bar; too many and a thin sample looks like noise. The reference also describes a curve option, which overlays a density curve on a histogram so you can tell what is in the data from what is in the binning. Whatever the setting, the filter on the grid still drives it: narrow the rows to one endpoint, or to the requests after a deploy, and the buckets are counted again from what is left.

Next

See the histogram beside a box plot in the distribution chart demo, read the full list of chart types on the charts page, or follow the how-to for a chart that follows the grid.

Read next

  • Pie Chart of Revenue Share by Channel

    Bind a pie chart to a grid of revenue by channel: show each channel as a share of the quarter, and filter the rows to redraw the slices.

  • Heatmap of Sign-ups by Hour and Weekday

    Bind a heatmap to a grid of sign-ups: shade every weekday and hour by volume to time a campaign send, and filter the rows to redraw it.

  • Violin Chart of Salary Bands by Level

    Bind a violin chart to a grid of salaries: see the shape of pay at each level, including two bands inside one, and filter the rows to redraw it.

All posts RSS