Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
163237 stories
·
33 followers

Why AI agents Cost So Much

1 Share
From: Microsoft Developer
Duration: 24:18
Views: 284

AI agents can quickly deliver business value, but as usage scales, many organizations are discovering that intelligent experiences come with real operational costs. In this episode of the Azure Essentials Show, host Takuto Higuchi sits down with Sindhura Bandhakavi, Director of Engineering, Microsoft CoreAI, to explore the emerging discipline of AI token economics, or "tokenomics," and what it takes to optimize agent performance without sacrificing quality.

Through practical demonstrations of capabilities such as Model Router and Agent Optimizer in Microsoft Foundry, they unpack the factors that drive AI costs, explain why reducing tokens alone is not always the right strategy, and show how organizations can treat AI as a managed investment. Whether you're building your first AI agent or looking to improve ROI across a growing portfolio of agents, this episode provides actionable guidance for balancing cost, performance, and scalability.

Three things you will learn
- Why AI agents can become expensive to operate at scale and the key factors that drive token consumption, model costs, and overall AI ROI. You'll learn why simply reducing tokens is not always the best optimization strategy.
- How to use Azure AI Foundry capabilities such as Model Router and Agent Optimizer to improve agent efficiency, automatically select the right model for each request, and continuously enhance agent performance over time.

- How to use Azure AI Foundry capabilities such as Model Router and Agent Optimizer to improve agent efficiency, automatically select the right model for each request, and continuously enhance agent performance over time.

- How to manage AI as a business investment by measuring costs, evaluating agent effectiveness, establishing controls for usage spikes, and ensuring every AI request delivers the right balance of quality, performance, and cost.
 
Resources 
- The economics of agent optimization: from pilots to measurable returns   https://azure.microsoft.com/blog/the-economics-of-agent-optimization-from-pilots-to-measurable-returns/
- Bridging the AI divide: How Frontier Firms are transforming business  https://blogs.microsoft.com/blog/2025/11/11/bridging-the-ai-divide-how-frontier-firms-are-transforming-business/
- Model router for Microsoft Foundry concepts   https://learn.microsoft.com/azure/foundry/openai/concepts/model-router
- How to use model router for Microsoft Foundry  https://learn.microsoft.com/azure/foundry/openai/how-to/model-router
- Agent optimizer in Foundry Agent Service overview https://learn.microsoft.com/azure/foundry/agents/concepts/agent-optimizer-overview
- Introducing Agent Optimizer in Foundry Agent Service  https://devblogs.microsoft.com/foundry/agent-optimizer-build2026/
- What's New in Microsoft Foundry, June 2026  https://devblogs.microsoft.com/foundry/whats-new-in-microsoft-foundry-june-2026/
- 2025 Stack Overflow Developer Survey https://survey.stackoverflow.co/2025/ai

Related episodes 
- Cut AI Agent Costs with TokenOps https://www.youtube.com/watch?v=B4yovi_znxE
- Getting Started with the Microsoft Agent Pre-Purchase Plan https://www.youtube.com/watch?v=0-RN85uZzTY
- Watch more episodes of the Azure Essentials Show https://aka.ms/AzureEssentialsShow

Connect 
-Takuto Higuchi https://www.linkedin.com/in/takuto-higuchi-626681135/
-Sindhura Bandhakavi https://www.linkedin.com/in/sindhura-bandhakavi/

 Chapters 
00:00 In this episode
00:26 Welcome to Tokenomics
01:00 Why AI Agents Cost More Than You Think
04:00 The Hidden Cost of Chasing Fewer Tokens
07:00 Measuring ROI for AI Agents
10:00 Choosing the Right Model with Model Router
12:00 Live Demo: Model Router in Action
15:30 Improving Agents Over Time with Agent Optimizer
18:00 Live Demo: Optimizing Prompts and Agent Performance
21:20 Building a Continuous Optimization Flywheel
22:20 Your AI Budget Review Checklist
23:16 Key Takeaways and Resources
22:53 Getting Started

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

First Public Working Draft: Animation Triggers

1 Share

The CSS Working Group has published a First Public Working Draft of Animation Triggers. It defines CSS properties and an API for creating mechanisms that affect animation playback, based on DOM events resulting from user interaction, or on entry/exit of ranges on animation timelines. 

CSS is a language for describing the rendering of structured documents (such as HTML and XML) on screen, on paper, etc.

Read the whole story
alvinashcraft
21 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Generative UI Without the Async Tax: Introducing bloc_signals_genui 1.0

1 Share

Wait—Another Generative UI Package?

"An asynchronous network stream belongs at the perimeter of your architecture; once an AI agent's UI lands on the client, every keystroke, validation rule, and leaf rebuild should execute in 0ms on a synchronous signal graph."
— Randal L. Schwartz

When you see a new Generative UI announcement for Dart and Flutter, your first reaction is probably healthy skepticism: Do we really need another GenUI wrapper?

Most Generative UI prototypes stop at the initial parlor trick: an LLM streams a JSON payload over Server-Sent Events (SSE), a widget tree appears on screen, and everyone claps. The architectural cracks appear five seconds later when a user tries to interact with that generated interface:

  • The Everything-Is-Async Tax: Because the LLM response arrives over an asynchronous network stream, most GenUI libraries wire the entire client lifecycle through StreamBuilder, async* generators, or asynchronous controller notifications. Every keystroke in a generated text field, every cross-field validation check, and every widget test pays a microtask and frame-delay tax.
  • Framework Lock-In: Most GenUI state engines are coupled directly to package:flutter/widgets.dart (BuildContext, Widget, ChangeNotifier), making it impossible to run or test the agent state machine in pure Dart, CLI tools, or terminal interfaces—and forcing you to adopt a bespoke state management paradigm that clashes with your existing Riverpod or flutter_bloc app.

Today, we are publishing v1.0.0 of bloc_signals_genui and bloc_signals_genui_flutter on pub.dev. Here is what sets them apart:

  • 100% Compliant with Google's Emerging A2UI Protocol: Built directly on Google's official package:a2ui_core (^0.3.0) library from the a2ui-project/a2ui specification (v0.9.1 and v1.0).
  • 0ms Synchronous Core & Live Two-Way Form Bindings: The async SSE stream is quarantined at the edge. Once on the client, two-way WritableBinding form inputs, synchronous CheckRule validation, and NodeResolver list templates execute in 0ms in the exact same frame.
  • True Pure-Dart Decoupling (From Flutter to Terminal TUIs): bloc_signals_genui has zero Flutter dependencies. The exact same A2uiSurfaceBloc state machine drives both a 120 FPS Material 3 Flutter app and an interactive ANSI Terminal User Interface (TUI).
  • Universal Interop (Keep Your Riverpod or Classic BLoC Core): Through bloc_signals_riverpod and bloc_signals_bloc, you can drop A2UI surfaces straight into an existing Riverpod or classic BLoC codebase without rewriting your architecture.

Foundation: Official A2UI Compliance and Shared Reactive DNA

We avoided inventing a custom JSON schema or maintaining a divergent protocol parser. Both packages build directly on Google's official package:a2ui_core Dart SDK for the A2UI (Agent-to-User Interface) protocol. (We even contributed upstream in a2ui-project/a2ui#2819 to lower a2ui_core's minimum SDK constraint to Dart ^3.5.0, opening the official A2UI engine to the entire Flutter 3.24+ ecosystem.)

When inspecting a2ui_core under the hood, one architectural detail stands out immediately: Google's a2ui_core already standardizes on preact_signals for its internal DataModel, GenericBinder, and reactive path bindings.

Wrapping a signal-powered protocol engine inside asynchronous StreamController pipes adds unnecessary translation layers. Because BlocSignal is built on the exact same reactive signal primitives, bloc_signals_genui connects to a2ui_core with zero impedance mismatch:

flowchart TD
    Agent["AI Agent / LLM Backend\n(Streams A2UI v0.9 / v1.0 JSON via SSE)"]
    Bloc["Edge Container: A2uiSurfaceBloc (Pure Dart)\n• Quarantines async stream with restartable() mutex\n• Sealed lifecycle: Initial → Streaming → Ready → Submitting\n• Bounded multi-surface LRU cache (maxSurfaces)"]
    Core["Synchronous Reactive Core (a2ui_core + BlocSignal)\n• 0ms Two-Way WritableBinding DataModel sync\n• Synchronous CheckRule validation (isValid / errorText)\n• Reactive NodeResolver ChildList template expansion"]
    Flutter["Flutter Renderer (bloc_signals_genui_flutter)\n• 16 Material 3 BasicCatalog widgets\n• Surgical leaf rebuilds via GenericBinder signals"]
    TUI["ANSI Terminal Renderer (examples/genui_tui_agent)\n• UTF-8 box drawing & bracketed hotkeys\n• Interactive stdin prompts & CheckRule badges"]
    Interop["Grand Central Interop\n• Riverpod 2/3 (.toProvider())\n• Classic BLoC & OpenTelemetry spans"]

    Agent -->|"1. Async JSON Chunks (Network Perimeter)"| Bloc
    Bloc -->|"2. 0ms Same-Frame Signal Graph"| Core
    Core --> Flutter
    Core --> TUI
    Core --> Interop

Pillar 1: A Synchronous Core with Live Two-Way Form Bindings

In a real application, an AI-generated surface functions as a live interactive form, not a static poster. When a flight-booking agent streams a checkout card, the user still needs to type passenger names, toggle baggage checkboxes, pick seat classes, and see instant validation feedback before submitting their selection back to the agent.

bloc_signals_genui and bloc_signals_genui_flutter support the full 16-component A2UI BasicCatalog (Text, Row, Column, List, Tabs, Modal, Card, Divider, Icon, Image, Button, TextField, CheckBox, ChoicePicker, Slider, and DateTimeInput) with four synchronous capabilities:

1. Two-Way WritableBinding Synchronization in 0ms

When an A2UI component binds a property to a JSON Pointer path in the surface's DataModel (for example, {"path": "/user/name"} in v0.9 or {"@path": "/user/name"} in v1.0), A2uiComponentContext exposes resolveBoundPath and updateBoundValue. Typing into an A2uiTextField, sliding an A2uiSlider, or picking a chip in A2uiChoicePicker mutates the underlying DataModel signal synchronously in 0ms. Only the leaf widget observing that path rebuilds—leaving the parent surface scaffold untouched.

2. Same-Frame CheckRule Validation UI

A2UI allows components to declare client-side checks rules right in the wire JSON. As the user edits a bound field, GenericBinder evaluates those CheckRule conditions synchronously on every keystroke. A2uiComponentContext exposes isValid and validationErrors, displaying inline errorText on invalid inputs and automatically disabling A2uiButton submissions (onPressed: null) in the exact same frame when required checks fail—with zero LLM round-trips.

3. Data-Driven NodeResolver List Templates

Instead of burning LLM output tokens streaming 20 repetitive component subtrees for a list of flights, the agent streams a single template component (flight_card) and a ChildList binding ({"componentId": "flight_card", "path": "/flights"}). A2uiSurfaceView mounts a2ui_core's NodeResolver to dynamically expand and scope each item's data context (/flights/0, /flights/1, ...) reactively as array elements stream in.

4. Bidirectional RPC (RpcHandler)

Beyond standard action submissions (SubmitAction), A2uiSurfaceBloc wires MessageProcessor.rpc so the agent can invoke client-side renderer functions (CallRendererFunctionMessage) and your Dart code can invoke agent-side functions via bloc.callAgentFunction(...) with built-in timeout and lifecycle cancellation safety.

import 'package:bloc_signals_genui/bloc_signals_genui.dart';

// 1. Create the pure-Dart A2UI state machine
final surfaceBloc = A2uiSurfaceBloc(
  maxSurfaces: 10,
  maxResponseHistory: 100,
);

// 2. Ingest an incoming A2UI v0.9 stream (quarantined by restartable())
surfaceBloc.add(IngestStream(llmChunkStream));

// 3. Mutate bound form fields synchronously in 0ms
surfaceBloc.add(
  const UpdateFormField(
    path: '/user/name',
    value: 'Randal L. Schwartz',
  ),
);

And because state transitions and form bindings are synchronous, your Flutter widget tests verify updates in a single tester.pump() without waiting on pumpAndSettle() microtask queues.

Pillar 2: Streamless Concurrency and Production Scars Built In

Streaming UI from an LLM introduces failure modes that traditional CRUD screens never encounter:

  1. Impatient Users & Mid-Stream Interruptions (restartable()): What happens when an agent is halfway through streaming a complex itinerary surface and the user changes their mind and sends a new prompt? In naive stream setups, both streams interleave and corrupt the component tree. A2uiSurfaceBloc guards IngestStream with BlocSignal's streamless restartable() mutex transformer—subscribing to the replacement stream, preempting the stale stream immediately, and cleaning up resources without race conditions.
  2. Zero-Chunk Stream Failures & Turn-Alternation Protection: Conversational APIs (such as Google Gemini and Firebase Genkit) enforce strict user → model turn alternation. If your app optimistically appends a user prompt to chat history and the network drops before the first SSE chunk arrives (chunkCount == 0), leaving that unfulfilled user turn in history causes the user's next retry to crash with INVALID_ARGUMENT: Consecutive user turns are not allowed. Our architecture and reference apps bake in transactional zero-chunk turn pruning out of the box.
  3. Multi-Surface Navigation & Bounded LRU Eviction: Agents frequently spawn multiple surfaces across a conversation (main workspace, comparison cards, detail modals). A2uiSurfaceBloc separates navigation coordinates (activeSurfaceId, a 0ms ReadonlySignal<String?>) from per-surface content revisions (_surfaceVersions), so switching tabs via SelectSurface never triggers redundant binder rebuilds in split-pane views—while maxSurfaces automatically evicts the least-recently-used non-active surfaces so long sessions never leak memory.

Pillar 3: Pure-Dart Decoupling—Running the Exact Same Agent in a Terminal TUI

Because bloc_signals_genui has zero dependency on Flutter, your entire Generative UI state machine can run in CLI tools, backend Dart servers, automated test harnesses, or terminal applications.

To prove this architecture in the monorepo, we built two complete reference applications powered by the exact same A2uiSurfaceBloc and BasicCatalog payloads:

  1. examples/genui_flight_booking: A Material 3 Flutter application rendering ChoicePicker seat chips, CheckBox preferences, DateTimeInput pickers, NodeResolver flight card templates (/flights), CheckRule passenger validation, and assignPreferredSeat RPC calls.
  2. examples/genui_tui_agent: An interactive ANSI Terminal User Interface (TUI) inspired by agy-cli that renders those exact same 16 BasicCatalog components, template paths, and CheckRule validation badges using UTF-8 box-drawing characters (┌─┐, │, └─┘), interactive stdin prompts, and bracketed action hotkeys ([1] Search Flights).
# Run the interactive ANSI Terminal TUI agent in pure Dart:
dart run examples/genui_tui_agent/bin/main.dart --mock

Decoupling the A2UI state machine from the renderer means you can verify and fuzz-test complex multi-turn agent flows in pure dart test in milliseconds, without spinning up a simulator or headless browser.

Pillar 4: "Grand Central Station" Interop—Keep Your Riverpod or Classic BLoC Core

Adopting a Generative UI library should not require rewriting your application's existing state management architecture.

Because A2uiSurfaceBloc extends BlocSignal, it plugs directly into the rest of the 13-package BlocSignal ecosystem:

  • Using Riverpod? Keep Your Riverpod Core: Add bloc_signals_riverpod and call .toProvider(autoClose: true) on A2uiSurfaceBloc. Your Riverpod ConsumerWidgets and providers can watch the A2UI surface state or dispatch events through ref while A2uiSurfaceView handles the granular signal rendering:
import 'package:bloc_signals_genui/bloc_signals_genui.dart';
import 'package:bloc_signals_riverpod/bloc_signals_riverpod.dart';
import 'package:riverpod/riverpod.dart';

// Expose A2uiSurfaceBloc as a native Riverpod NotifierProvider
final surfaceBloc = A2uiSurfaceBloc(maxSurfaces: 5);
final a2uiSurfaceProvider = surfaceBloc.toProvider(autoClose: true);

// Inside any Riverpod consumer:
// final surfaceState = ref.watch(a2uiSurfaceProvider);
// ref.read(a2uiSurfaceProvider.notifier).bloc.add(IngestStream(stream));
  • Using Classic flutter_bloc? Bridge It cleanly: Use bloc_signals_bloc to bridge events and states bidirectionally with classic Bloc and Cubit instances.
  • Want Production Telemetry? Attach OpenTelemetry & DevTools: Drop in bloc_signals_otel and bloc_signals_devtools to trace every A2UI stream ingestion, chunk transition, and restartable() preemption span in OpenTelemetry and the Flutter DevTools extension with zero changes to your UI code.

Get Started Today

Both bloc_signals_genui (1.0.0) and bloc_signals_genui_flutter (1.0.0) are live on pub.dev with 100% test coverage and full support for Dart 3.5+ through Dart 3.13+:

# For pure-Dart, CLI, or TUI applications:
dart pub add bloc_signals_genui bloc_signals

# For Flutter applications:
flutter pub add bloc_signals_genui_flutter bloc_signals_genui bloc_signals_flutter

In follow-up articles in this series, we will dive deeper into each of these four pillars—including a hands-on walkthrough of building two-way validated A2UI forms with WritableBinding and CheckRule, wiring NodeResolver templates, and building headless/TUI agent runners in pure Dart.

Links & Resources

Read the whole story
alvinashcraft
27 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Coding Agents Guess Which Tests to Run. They Should Look It Up.

1 Share

I’ve been updating trunkbaseddevelopment.com for coding agents, with Claude doing much of the typing. One section is now about AI coding agents committing straight to the trunk. That’s what you get if you remove the pull-request requirement and let a diaspora of agents in worktrees, VMs and containers race each other to HEAD. Those agents need fast local builds to win that race. Fast local builds on a big codebase need Test Impact Analysis (TIA). So I asked Claude a simple question: when you’re working in a repo with a too-large test suite, how do you pick which tests to run?

What a coding agent actually does

The answer was candid. Unless the repo provides it, there’s no coverage map and no record of which tests touched which lines last time. There’s no familiarity carried over from yesterday’s session either, unless someone wrote it into a CLAUDE.md or a memory file. What it does instead is reason from the diff:

  1. By naming conventions: Foo.java to FooTest.java, foo.py to test_foo.py, a __tests__ folder next door.
  2. By who uses what changed: grep for the changed symbols and take the callers, importers and subclasses, one or two hops out, and their tests.
  3. By module scope: go test ./pkg/thing/..., mvn -pl module, a single pytest directory.
  4. By graph-aware tooling where it exists: bazel query 'rdeps(...)', nx affected, jest --findRelatedTests. It trusts these most, because they are real dependency data and not its own reading of the code.
  5. By widening when unsure: a change to a base class, config or serialization means running the bigger subset, or everything.

That’s TIA by inference. It’s the same thing a decent senior developer does in their head, and to be fair it’s quite good. Where it goes wrong is predictable: coupling that grep can’t see. Reflection, dependency injection wiring, annotation- and config-driven behaviour, string-keyed lookups, serialized formats, SQL, anything across a process boundary. A coverage-derived map catches those because it records what actually ran. The agent is inferring from text.

Claude also admitted to a behavioural risk. Like a hurried human, an agent under pressure to finish drifts towards running too few tests and declaring victory. The protection is the same as for the hurried human: the full suite runs in CI after the commit lands, and the agent says plainly which subset it ran.

Its own conclusion was the one I’d hoped for: given a real TIA map, it would rather query that than guess. TIA turns test selection from the agent’s judgement into data it can look up. Agents committing at volume may be the strongest argument for TIA yet.

Well of course, the pace of development of agentic coding means these gripes will be out of date soon, but it’s worth captureing the here and now.

Recap: what TIA is

I wrote The Rise of Test Impact Analysis for Martin Fowler’s site, and before that Reducing Test Times by Only Running Impacted Tests here. Briefly: run the suite with coverage instrumentation, record which production source files each test exercises, and store that as a map. For a given change, look up the tests that touch the changed files and run only those before the commit/push. The full suite still runs in CI afterwards. The subset is usually a small fraction of the whole, so the pre-push build gets fast again without anyone deleting tests or skipping the build.

DAG build systems like Bazel, Buck and Nx get much of the same benefit from their dependency graphs. I went through that in Google-style DAG build systems. TIA is the route for everyone using Maven, Gradle, MSBuild, npm and the rest.

The catch: maps are costly

TIA maps aren’t free:

  • Building one is slow: You run the whole suite instrumented, ideally per test, which is slower than a normal run.
  • They go stale, quickly:: Every commit makes the map slightly out of date. Most teams settle for rebuilding it nightly, or every N commits.
  • Costly plumbing: It has to be stored, keyed to commits, and fetched by whoever (or whatever) wants it.
  • Coverage is noisy: The same test can cover slightly different lines from run to run. This is especially true in legacy codebases.

The staleness tradeoff is reasonable, though. A stale map mostly misses brand-new tests and newly introduced coupling, and the full CI run after the commit catches those soon after. A map that is a day old and right 98% of the time beats a guess, and CI is the backstop for the other 2%, unless your CI is using the same TIA map for an elapsed time or “cloud minutes” reduction too.

Where TIA pays, and where it doesn’t

How much TIA saves depends on the shape of the codebase.

Good fit: Business applications are wide and shallow.

Hundreds of features, each test exercising a thin slice through them, and most changes touching one slice. Change the discount rules and you have a few dozen impacted tests out of thousands. The map pays for itself many times over. This is most of the enterprise software in the world, and most of what coding agents are being pointed at.

Bad fit: Programming-language implementations are narrow and deep.

Compilers, interpreters and VMs put nearly every test through the same core: lexer, parser, type checker, code generator, runtime. Change anything in that core and the map says “run everything”. That’s correct, but useless. TIA only helps there for changes at the edges, like a single standard library function or one diagnostic message. The tests are often data rather than code, too: source files in the language, with golden outputs. The useful question is “which language features does this input use?”, not “which test methods hit this line?”.

Language repos have their own techniques instead:

  • Tiered suites: a small, fast smoke/conformance set before the push, with the big corpus left to CI.
  • Tagging tests by language feature: touch closure codegen, run the closure tests. That’s TIA done by hand from domain knowledge rather than from coverage. Coding agents happen to be decent at it.
  • Parallelism and sharding: when selection can’t cut the work, spread it out.

Not yet an observed habit

On trunkbaseddevelopment.com I have a page of observed habits of teams practicing Trunk-Based Development. Running the build locally before the push is one of them, and has been since the late 90’s. TIA isn’t one yet. It’s been possible for over a decade, and the people who adopted it rate it, but I can’t honestly say it’s something you see in most trunk-based teams making business solutions.

It should be. Agents are now doing much of the committing, many of them at once, all chasing the same HEAD. The local build is what keeps the trunk green, and the speed of the local build is what decides who wins the race to commit. An agent that guesses which tests to run is doing what we always did. An agent that can look it up is doing what we always should have.

Read the whole story
alvinashcraft
47 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Semicolon is on the App Store

1 Share

Semicolon is out. It’s an iPhone and iPad app for practicing the parts of programming that interviews still test.

The Semicolon course list on iPhone, showing eight programming courses with ARM64 Assembly and Python free

More and more code gets written with an LLM’s help, and I don’t expect that to change. A company can assume you’ll use a model, but it still needs someone who can read what the model wrote, find the bug in it, and decide whether it’s any good. That takes a solid grasp of the fundamentals, and fundamentals fade quickly once you stop typing them out yourself. Semicolon is a way to keep them sharp.

Figuring out what a seasoned developer should know is a hard problem. I talked to many other senior software engineers to come up with this list of skills. They demonstrate competence, and they’re the fundamentals that help you work with LLMs more effectively.

What’s in it

There are eight courses with 24 sections each: ARM64 assembly, Python, Rust, Kotlin, SwiftUI, discrete math, design patterns, and data structures and algorithms.

I picked them because each one shows you understand something essential.

Assembly and Rust are about what the machine is doing. In assembly you work with registers and memory directly. In Rust you have to understand ownership before the compiler will let you do anything. I chose ARM64 because that’s the architecture in phones and in Apple silicon.

Python is what most people reach for when they work with a model, and it’s a common language for interviews.

Data structures and algorithms, discrete math, and design patterns are the material interviews have always drawn on. They’re also the vocabulary you use to talk to a model about code and to judge what it gives back.

Kotlin and SwiftUI cover the two mobile platforms. I expect local models to end up running on phones over the next few years, so these are the languages you’d use to build for them.

How it works

Every exercise uses real code. Depending on the lesson, you might pick the right answer, fill in a blank, fix a bug, predict what a snippet prints, or write a function. After each answer you get an explanation of why it’s right or wrong. A concept page is one tap away if you get stuck mid-exercise. If you already know a section, you can test out of it with a timed challenge.

Lessons are short, so a lesson a day is realistic.

Price

ARM64 assembly and Python are free. A single purchase of $24.99 adds the other six courses. There’s no subscription, no ads, and no account to create. Your progress stays on your device. There are no hearts or lives and no streaks to protect.

Try it

Semicolon: Learn to Code on the App Store

If you try it and like it, a rating on the App Store helps a lot. If something breaks or a lesson is wrong, the support page is the way to reach me. More about the app is on the Semicolon page.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete

Event Sourcing: Is our Projection Code fast enough?

1 Share

In this part of my event sourcing series, we take a quick, pun intended, look at how fast the projector we saw in the last post actually runs. The projector gets a list of events as input and projects them into a single value, representing the latest state. This code runs a couple hundred times a second in our system, so it needs to run fast.

The benchmark result

I ran a Benchmark.Net benchmark on my machine, with the following results:

The different cases:

  • CreateOnly: only a single create event exists in the event stream
  • Many: 1 create event, followed by 500 update events in the event stream
  • WithDeletes: a create event, an update event, and a delete event
  • WithOutOfOrderEvents: a create event, 4 update events in incorrect order, and a delete event -> events need sorting
  • WithOverrides: a create event, followed by 3 update events

These cover our typical cases.

Is it fast enough?

To answer this question, we need to look at what else is typically happing around the pure projection:

  • get the events from the database (typically takes 6 to 20ms in our production system)
  • project the events (as see above, typically below a micro second)
  • compute something based on these events (nano seconds to milli seconds)

So the projection times really don’t matter much in the whole picture. Allocations are probably the bigger concern because of garbage collection runs when Gen 1 memory is used (I’m looking forward to .Net 11 and the corresponding F# version to improve this).

Deep dive: the benchmark code

I dump the whole code here. When tinkering with the projection code, I add a second [Benchmark] that calls a duplicate of the original projection code so that I can compare the old and new versions:

namespace Benchmark.CreateProjection

open System
open BenchmarkDotNet.Attributes
open Calitime
open Calitime.TimeRocket
open Calitime.TimeRocket.Projections

open Calitime.TimeRocket.Projections.ValueProjection
open Calitime.Zeit

type EventId = EventId of Guid

type Override = { Value : string }

type EventData =
    | Created
    | Override of Override
    | Delete
    | Rejected of RelatedEventId : EventId

type Event =
    {
        Id : EventId
        Application : Application
        Data : EventData
    }

type Projection = { Value : string }

type Override2 = { Value : string }


[<MemoryDiagnoser>]
type CreateProjectionBenchmark() =

    let projectAction (event : Event) =
        match event.Data with
        | Created -> CreateProjectionAction.Creates { Projection.Value = "created" }
        | Override o -> CreateProjectionAction.Updates (fun _ -> { Projection.Value = o.Value })
        | Delete -> CreateProjectionAction.Deletes
        | Rejected _ -> CreateProjectionAction.Skip

    let configuration =
        {
            ProjectionActionConfiguration = CreateUpdateDelete projectAction
            GetApplication = fun e -> e.Application
        }

    let eventsFSharpCreatesOnly =
        [
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "18.01.2021 08:00"
                Data = Created
            }
        ]

    let eventsFSharpWithOverrides =
        [
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "18.01.2021 08:00"
                Data = Created
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "19.01.2021 08:00"
                Data = Override { Override.Value = "1" }
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "20.01.2021 08:00"
                Data = Override { Override.Value = "2" }
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "21.01.2021 08:00"
                Data = Override { Override.Value = "3" }
            }
        ]

    let eventsFSharpWithDeletes =
        [
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "18.01.2021 08:00"
                Data = Created
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "19.01.2021 08:00"
                Data = Override { Override.Value = "1" }
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "20.01.2021 08:00"
                Data = Delete
            }
        ]

    let eventsFSharpWithOutOfOrderEvents =
        [
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "18.01.2021 08:00"
                Data = Created
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "20.01.2021 08:00"
                Data = Override { Override.Value = "2" }
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "19.01.2021 08:00"
                Data = Override { Override.Value = "1" }
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "22.01.2021 08:00"
                Data = Override { Override.Value = "4" }
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "21.01.2021 08:00"
                Data = Override { Override.Value = "3" }
            }
            {
                Event.Id = Guid.NewGuid () |> EventId
                Application = Application.parse "23.01.2021 08:00"
                Data = Delete
            }
        ]

    let manyEvents =
        [
            {
                Event.Id = Guid.generateNew ()
                Application = Application.parse "18.01.2021 08:00"
                Data = Created
            }
            for i in 1 .. 500 do
                {
                    Event.Id = Guid.generateNew ()
                    Application = Application.parse "18.01.2021 08:00" |> fun (Application time) -> time.PlusTicks i |> Application
                    Data = Override { Value = i |> string } 
                }
        ]


    [<Params("CreateOnly", "WithOverrides", "WithDeletes", "WithOutOfOrderEvents", "Many")>]
    member val Case = "" with get, set

    member val FSharpEvents : Event list = [] with get, set

    [<GlobalSetup>]
    member this.GlobalSetup() =
        this.FSharpEvents <-
            match this.Case with
            | "CreateOnly" -> eventsFSharpCreatesOnly
            | "WithOverrides" -> eventsFSharpWithOverrides
            | "WithDeletes" -> eventsFSharpWithDeletes
            | "WithOutOfOrderEvents" -> eventsFSharpWithOutOfOrderEvents
            | "Many" -> manyEvents
            | _ -> failwith "unknown value for Case"

    [<Benchmark(Baseline = true)>]
    member this.FSharp() =
        let _ = ValueProjector.project configuration (this.FSharpEvents |> EventsForProjection)
        ()
Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories