Skip to main content
All Examples

Slow Warnings

Interactive lab for triggering slow update, view, patch, and Subscription dependency warnings with default thresholds and a visible warning log.

Performance
Diagnostics
/
import {
  Array,
  Effect,
  Match as M,
  Number,
  Option,
  Queue,
  Schema as S,
  Stream,
  pipe,
} from 'effect'
import { Command, Runtime, Subscription } from 'foldkit'
import { type Document, type Html, createLazy, html } from 'foldkit/html'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'

const UPDATE_WORK_MS = 10
const VIEW_WORK_MS = 24
const SUBSCRIPTION_DEPENDENCIES_WORK_MS = 8
const PATCH_ROW_COUNT = 4000
const MAX_WARNING_COUNT = 8
const SLOW_WARNING_EVENT = 'foldkit:slow-warning'

const Workload = S.Literals([
  'Idle',
  'Update',
  'View',
  'Patch',
  'SubscriptionDependencies',
])
type Workload = typeof Workload.Type

type SlowPhase = Runtime.SlowPhase

export const SlowWarningReport = S.Struct({
  phase: Runtime.SlowPhase,
  durationMs: S.Number,
  thresholdMs: S.Number,
  trigger: S.String,
  details: S.String,
})
export type SlowWarningReport = typeof SlowWarningReport.Type

export const SlowWarning = S.Struct({
  id: S.Number,
  ...SlowWarningReport.fields,
})
export type SlowWarning = typeof SlowWarning.Type

// MODEL

export const Model = S.Struct({
  activeWorkload: Workload,
  nextWarningId: S.Number,
  warnings: S.Array(SlowWarning),
  patchRows: S.Number,
  patchRun: S.Number,
})
export type Model = typeof Model.Type

// MESSAGE

export const ClickedRunUpdateWork = m('ClickedRunUpdateWork')
export const ClickedRunViewWork = m('ClickedRunViewWork')
export const ClickedRunPatchWork = m('ClickedRunPatchWork')
export const ClickedRunSubscriptionDependenciesWork = m(
  'ClickedRunSubscriptionDependenciesWork',
)
export const ClickedClearWarnings = m('ClickedClearWarnings')
export const RecordedSlowWarning = m('RecordedSlowWarning', {
  report: SlowWarningReport,
})

export const Message = S.Union([
  ClickedRunUpdateWork,
  ClickedRunViewWork,
  ClickedRunPatchWork,
  ClickedRunSubscriptionDependenciesWork,
  ClickedClearWarnings,
  RecordedSlowWarning,
])
export type Message = typeof Message.Type

const slowWarningTarget = new EventTarget()

const burnCpu = (durationMs: number): number => {
  const stopAt = performance.now() + durationMs
  let checksum = 0

  while (performance.now() < stopAt) {
    checksum = (checksum + Math.sqrt(checksum + 1)) % 100000
  }

  return checksum
}

const messageToTag = ({ _tag }: Message): string => _tag

const maybeMessageTrigger = (message: Option.Option<Message>): string =>
  Option.match(message, {
    onNone: () => 'initial render',
    onSome: messageToTag,
  })

const triggerForSlowContext = (
  context: Runtime.SlowContext<Model, Message>,
): string =>
  M.value(context).pipe(
    M.withReturnType<string>(),
    M.tagsExhaustive({
      Update: ({ message }) => messageToTag(message),
      View: ({ message }) => maybeMessageTrigger(message),
      Patch: ({ message }) => maybeMessageTrigger(message),
      SubscriptionDependencies: ({ subscriptionKey }) => subscriptionKey,
    }),
  )

const detailsForSlowContext = (
  context: Runtime.SlowContext<Model, Message>,
): string =>
  M.value(context).pipe(
    M.withReturnType<string>(),
    M.tagsExhaustive({
      Update: () =>
        'CPU work ran inside update before Foldkit could return the next Model.',
      View: () =>
        'The view function performed expensive synchronous work while building the next VNode tree.',
      Patch: () =>
        'The patch phase inserted thousands of keyed rows into the live DOM.',
      SubscriptionDependencies: () =>
        'A subscription spent time deriving its dependency struct from the Model.',
    }),
  )

const phaseLabel = (phase: SlowPhase): string =>
  M.value(phase).pipe(
    M.withReturnType<string>(),
    M.when('Update', () => 'Update'),
    M.when('View', () => 'View'),
    M.when('Patch', () => 'Patch'),
    M.when('SubscriptionDependencies', () => 'Subscription dependencies'),
    M.exhaustive,
  )

const slowContextToReport = (
  context: Runtime.SlowContext<Model, Message>,
): SlowWarningReport => ({
  phase: context._tag,
  durationMs: context.durationMs,
  thresholdMs: context.thresholdMs,
  trigger: triggerForSlowContext(context),
  details: detailsForSlowContext(context),
})

export const handleSlow = (
  context: Runtime.SlowContext<Model, Message>,
): void => {
  Runtime.defaultSlowCallback(context)

  const report = slowContextToReport(context)
  slowWarningTarget.dispatchEvent(
    new CustomEvent<SlowWarningReport>(SLOW_WARNING_EVENT, {
      detail: report,
    }),
  )
}

// UPDATE

const prependWarning =
  (warning: SlowWarning) =>
  (warnings: ReadonlyArray<SlowWarning>): ReadonlyArray<SlowWarning> =>
    pipe(warnings, Array.prepend(warning), Array.take(MAX_WARNING_COUNT))

type UpdateReturn = readonly [Model, ReadonlyArray<Command.Command<Message>>]

export const update = (model: Model, message: Message): UpdateReturn =>
  M.value(message).pipe(
    M.withReturnType<UpdateReturn>(),
    M.tagsExhaustive({
      ClickedRunUpdateWork: () => {
        burnCpu(UPDATE_WORK_MS)

        return [
          evo(model, {
            activeWorkload: () => 'Update',
          }),
          [],
        ]
      },
      ClickedRunViewWork: () => [
        evo(model, {
          activeWorkload: () => 'View',
        }),
        [],
      ],
      ClickedRunPatchWork: () => [
        evo(model, {
          activeWorkload: () => 'Patch',
          patchRows: () => PATCH_ROW_COUNT,
          patchRun: Number.increment,
        }),
        [],
      ],
      ClickedRunSubscriptionDependenciesWork: () => [
        evo(model, {
          activeWorkload: () => 'SubscriptionDependencies',
        }),
        [],
      ],
      ClickedClearWarnings: () => [
        evo(model, {
          activeWorkload: () => 'Idle',
          warnings: () => [],
        }),
        [],
      ],
      RecordedSlowWarning: ({ report }) => {
        const warning: SlowWarning = {
          id: model.nextWarningId,
          ...report,
        }

        return [
          evo(model, {
            activeWorkload: () => 'Idle',
            nextWarningId: Number.increment,
            warnings: prependWarning(warning),
          }),
          [],
        ]
      },
    }),
  )

// INIT

export const init: Runtime.ApplicationInit<Model, Message> = () => [
  {
    activeWorkload: 'Idle',
    nextWarningId: 1,
    warnings: [],
    patchRows: 0,
    patchRun: 0,
  },
  [],
]

// SUBSCRIPTION

export const subscriptions = Subscription.make<Model, Message>()(entry => ({
  slowWarnings: Subscription.persistent(
    Stream.callback<typeof RecordedSlowWarning.Type>(queue =>
      Effect.acquireRelease(
        Effect.sync(() => {
          const handler = (event: Event) => {
            if (event instanceof CustomEvent) {
              pipe(
                event.detail,
                S.decodeUnknownOption(SlowWarningReport),
                Option.match({
                  onNone: () => undefined,
                  onSome: report =>
                    Queue.offerUnsafe(queue, RecordedSlowWarning({ report })),
                }),
              )
            }
          }

          slowWarningTarget.addEventListener(SLOW_WARNING_EVENT, handler)
          return handler
        }),
        handler =>
          Effect.sync(() =>
            slowWarningTarget.removeEventListener(SLOW_WARNING_EVENT, handler),
          ),
      ).pipe(Effect.flatMap(() => Effect.never)),
    ),
  ),
  burnCpuDuringDependencyExtraction: entry(
    {
      activeWorkload: Workload,
    },
    {
      modelToDependencies: model => {
        if (model.activeWorkload === 'SubscriptionDependencies') {
          burnCpu(SUBSCRIPTION_DEPENDENCIES_WORK_MS)
        }

        return {
          activeWorkload: model.activeWorkload,
        }
      },
      dependenciesToStream: () => Stream.empty,
    },
  ),
}))

// VIEW

const lazyPatchRows = createLazy()

const buttonClass =
  'inline-flex items-center justify-center rounded-md bg-zinc-950 px-3 py-2 text-sm font-semibold text-white transition hover:bg-zinc-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-950'

const secondaryButtonClass =
  'inline-flex items-center justify-center rounded-md border border-zinc-300 bg-white px-3 py-2 text-sm font-semibold text-zinc-900 transition hover:bg-zinc-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-950'

const phaseAccentClass = (phase: SlowPhase): string =>
  M.value(phase).pipe(
    M.withReturnType<string>(),
    M.when('Update', () => 'border-amber-400 bg-amber-50 text-amber-950'),
    M.when('View', () => 'border-sky-400 bg-sky-50 text-sky-950'),
    M.when('Patch', () => 'border-rose-400 bg-rose-50 text-rose-950'),
    M.when(
      'SubscriptionDependencies',
      () => 'border-emerald-400 bg-emerald-50 text-emerald-950',
    ),
    M.exhaustive,
  )

const burnCpuDuringView = (workload: Workload): void => {
  if (workload === 'View') {
    burnCpu(VIEW_WORK_MS)
  }
}

const scenarioCard = ({
  phase,
  thresholdMs,
  title,
  body,
  buttonText,
  message,
}: Readonly<{
  phase: SlowPhase
  thresholdMs: number
  title: string
  body: string
  buttonText: string
  message: Message
}>): Html => {
  const h = html<Message>()

  return h.article(
    [h.Class(`rounded-lg border p-4 shadow-sm ${phaseAccentClass(phase)}`)],
    [
      h.div(
        [h.Class('mb-4')],
        [
          h.div(
            [],
            [
              h.h2([h.Class('text-base font-bold')], [title]),
              h.p(
                [h.Class('mt-1 text-sm opacity-80')],
                [`Default threshold: ${thresholdMs}ms`],
              ),
            ],
          ),
        ],
      ),
      h.p([h.Class('mb-4 text-sm leading-6')], [body]),
      h.button(
        [h.Type('button'), h.Class(buttonClass), h.OnClick(message)],
        [buttonText],
      ),
    ],
  )
}

const warningView = (warning: SlowWarning): Html => {
  const h = html<Message>()

  return h.keyed('li')(
    warning.id.toString(),
    [
      h.Class(
        `rounded-lg border p-4 shadow-sm ${phaseAccentClass(warning.phase)}`,
      ),
    ],
    [
      h.div(
        [h.Class('flex flex-wrap items-baseline justify-between gap-2')],
        [
          h.h3(
            [h.Class('text-base font-bold')],
            [`${phaseLabel(warning.phase)} exceeded ${warning.thresholdMs}ms`],
          ),
          h.p(
            [h.Class('text-sm font-semibold')],
            [`${warning.durationMs.toFixed(1)}ms`],
          ),
        ],
      ),
      h.p(
        [h.Class('mt-2 text-sm leading-6')],
        [`${warning.details} Trigger: ${warning.trigger}.`],
      ),
    ],
  )
}

const warningsView = (warnings: ReadonlyArray<SlowWarning>): Html => {
  const h = html<Message>()

  return h.section(
    [h.Class('rounded-lg border border-zinc-200 bg-white p-4 shadow-sm')],
    [
      h.div(
        [h.Class('mb-4 flex flex-wrap items-center justify-between gap-3')],
        [
          h.div(
            [],
            [
              h.h2(
                [h.Class('text-lg font-bold text-zinc-950')],
                ['Recorded warnings'],
              ),
              h.p(
                [h.Class('text-sm text-zinc-600')],
                ['Warnings here come from the real Runtime slow callback.'],
              ),
            ],
          ),
          h.button(
            [
              h.Type('button'),
              h.Class(secondaryButtonClass),
              h.OnClick(ClickedClearWarnings()),
            ],
            ['Clear'],
          ),
        ],
      ),
      h.keyed('div')(
        Array.isReadonlyArrayEmpty(warnings) ? 'empty' : 'warnings',
        [],
        Array.isReadonlyArrayEmpty(warnings)
          ? [
              h.div(
                [
                  h.Class(
                    'rounded-lg border border-dashed border-zinc-300 bg-zinc-50 p-6 text-sm text-zinc-600',
                  ),
                ],
                ['Run a workload to record a warning.'],
              ),
            ]
          : [h.ul([h.Class('grid gap-3')], Array.map(warnings, warningView))],
      ),
    ],
  )
}

const patchRowsView = (rowCount: number, patchRun: number): Html => {
  const h = html<Message>()

  if (rowCount === 0) {
    return h.keyed('div')(
      'empty',
      [
        h.Class(
          'rounded-lg border border-dashed border-zinc-300 bg-zinc-50 p-6 text-sm text-zinc-600',
        ),
      ],
      ['Patch rows will appear here.'],
    )
  } else {
    return h.keyed('div')(
      `rows-${patchRun}`,
      [h.Class('grid max-h-80 gap-1 overflow-auto pr-2')],
      Array.map(Array.range(1, rowCount), row =>
        h.keyed('div')(
          `patch-row-${patchRun}-${row}`,
          [
            h.Class(
              'flex items-center justify-between rounded-md border border-zinc-200 bg-white px-3 py-2 text-xs text-zinc-600',
            ),
          ],
          [
            h.span([], [`Patch row ${row}`]),
            h.span([h.Class('font-mono')], [`run ${patchRun}`]),
          ],
        ),
      ),
    )
  }
}

const patchSurfaceView = (model: Model): Html => {
  const h = html<Message>()

  return h.section(
    [h.Class('rounded-lg border border-zinc-200 bg-white p-4 shadow-sm')],
    [
      h.div(
        [h.Class('mb-4 flex flex-wrap items-center justify-between gap-3')],
        [
          h.div(
            [],
            [
              h.h2(
                [h.Class('text-lg font-bold text-zinc-950')],
                ['Patch surface'],
              ),
              h.p(
                [h.Class('text-sm text-zinc-600')],
                [`${model.patchRows.toLocaleString()} keyed rows mounted`],
              ),
            ],
          ),
        ],
      ),
      lazyPatchRows(patchRowsView, [model.patchRows, model.patchRun]),
    ],
  )
}

export const view = (model: Model): Document => {
  const h = html<Message>()

  burnCpuDuringView(model.activeWorkload)

  return {
    title: 'Slow Warnings Lab',
    body: h.main(
      [h.Class('min-h-screen bg-zinc-50 text-zinc-950')],
      [
        h.header(
          [h.Class('border-b border-zinc-200 bg-white')],
          [
            h.div(
              [h.Class('mx-auto max-w-6xl px-5 py-6')],
              [
                h.h1(
                  [h.Class('text-3xl font-bold tracking-normal text-zinc-950')],
                  ['Slow Warnings Lab'],
                ),
                h.p(
                  [h.Class('mt-2 max-w-3xl text-zinc-600')],
                  [
                    'Each workload intentionally blocks one part of the synchronous update cycle long enough to trip the default threshold.',
                  ],
                ),
              ],
            ),
          ],
        ),
        h.div(
          [h.Class('mx-auto grid max-w-6xl gap-6 px-5 py-6')],
          [
            h.section(
              [h.Class('grid gap-4 md:grid-cols-2')],
              [
                scenarioCard({
                  phase: 'Update',
                  thresholdMs: 4,
                  title: 'Slow update',
                  body: 'Runs CPU work before returning the next Model.',
                  buttonText: 'Run update work',
                  message: ClickedRunUpdateWork(),
                }),
                scenarioCard({
                  phase: 'View',
                  thresholdMs: 16,
                  title: 'Slow view',
                  body: 'Runs CPU work while the view builds the VNode tree.',
                  buttonText: 'Run view work',
                  message: ClickedRunViewWork(),
                }),
                scenarioCard({
                  phase: 'Patch',
                  thresholdMs: 8,
                  title: 'Slow patch',
                  body: 'Mounts thousands of keyed rows into the DOM.',
                  buttonText: 'Run patch work',
                  message: ClickedRunPatchWork(),
                }),
                scenarioCard({
                  phase: 'SubscriptionDependencies',
                  thresholdMs: 2,
                  title: 'Slow subscription dependencies',
                  body: 'Burns CPU while deriving subscription dependencies.',
                  buttonText: 'Run dependency extraction',
                  message: ClickedRunSubscriptionDependenciesWork(),
                }),
              ],
            ),
            warningsView(model.warnings),
            patchSurfaceView(model),
          ],
        ),
      ],
    ),
  }
}