function splitBlock(block) {
  for (let col of block.columns) col.decoder.reset()

  // Make each of the resulting blocks between 50% and 80% full (leaving a bit of space in each
  // block so that it doesn't get split again right away the next time an operation is added).
  // The upper bound cannot be lower than 75% since otherwise we would end up with a block less than
  // 50% full when going from two to three blocks.
  const numBlocks = Math.ceil(block.numOps / (0.8 * MAX_BLOCK_SIZE))
  let blocks = [], opsSoFar = 0

  for (let i = 1; i <= numBlocks; i++) {
    const opsToCopy = Math.ceil(i * block.numOps / numBlocks) - opsSoFar
    const encoders = block.columns.map(col => ({columnId: col.columnId, encoder: encoderByColumnId(col.columnId)}))
    copyColumns(encoders, block.columns, opsToCopy)
    const decoders = encoders.map(col => {
      const decoder = decoderByColumnId(col.columnId, col.encoder.buffer)
      return {columnId: col.columnId, decoder}
    })

    const newBlock = {columns: decoders}
    updateBlockMetadata(newBlock)
    blocks.push(newBlock)
    opsSoFar += opsToCopy
  }

  return blocks
}
Takes a block containing too many operations, and splits it into a sequence of adjacent blocks of
roughly equal size.