Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Row mode and the RowModeReporter hook

Trill runs most operators in columnar mode. It stores each batch of events as one array per field and generates code for the operator that works on those arrays. When an operator cannot do that, it falls back to row mode, which handles one event object at a time. Row mode gives the same results, and it is usually slower.

Fallback is automatic. Before this hook was added, a fallback left a trace in the operator’s ErrorMessages only when code generation failed. The hook reports each fallback as it happens, with its reason.

The diagrams on this site run every query in row mode on purpose, through Config.ForceRowBasedExecution, because the browser cannot compile the generated code. The hook is not called when that setting is on.

Setting the hook

Config.RowModeReporter is a static Action<RowModeReport>. Set it before you build your queries:

Config.RowModeReporter = report =>
    Console.WriteLine($"{report.Operator.GetType().Name}: {report.Reason} ({report.Detail})");

For example, this payload type has a read-only field:

public sealed class Reading { public readonly string Sensor; public int Value; }

Building a query with this payload type reports the ingress with the reason TypeNotColumnar and the detail field Sensor is read-only. Downstream operators generally run in row mode too, without reports of their own, because their input is already in rows.

Some operators directly after the ingress do trigger another report. For an ingress built by calling ToStreamable() on StreamEvents, Trill fuses certain operators directly after it into a new ingress. These are Where, Select and the constant-duration overload AlterEventDuration(n). The new ingress reports the same reason again. The AlterEventDuration overloads that take a function are not fused, and add no report.

What a report contains

PropertyWhat it holds
OperatorThe operator that falls back: the IStreamable instance, or, for egress, the observable the egress method returned. It is typed object, so match on its type name.
ReasonOne of the four reasons below.
NonColumnarTypeFor TypeNotColumnar, the type that failed the check. It may be the operator’s input, its output or another type it holds. Otherwise null.
DetailA plain-language reason, or null if the operator recorded none. For TypeNotColumnar, the rule the type broke.
DiagnosticsDiagnostics recorded during code generation, or null. They include the compiler’s diagnostics if compilation ran.
ExceptionThe exception that stopped code generation, or null.
AtSubscribeTrue if the fallback decision was made during a subscription that Trill tracks: through an egress method or Cache(). False if it was made while building the query or during an untracked subscription. One untracked path is a direct call to IStreamable.Subscribe, which is intended for internal use.

report.ToString() gives a one-line summary, such as Row mode: <operator> (TypeNotColumnar): field Sensor is read-only.

The four reasons

ReasonWhenLook at
TypeNotColumnarA type the operator would hold in columns cannot be represented as columns.NonColumnarType and Detail
CodeGenerationFailedTrill tried to generate the operator’s columnar code and failed.Diagnostics and Exception
PartitionedStreamThe stream is partitioned, and the operator has no columnar form for partitioned streams.Detail is “partitioned streams run in row mode”
NotSupportedThe operator has no columnar form for this query.Detail, when the operator recorded a reason; otherwise null

Types that cannot be held in columns

For TypeNotColumnar, Detail names the first rule the type breaks. Trill stops at the first failure. It follows the table from top to bottom, treating the three consecutive property checks as a group. For each property, it checks for a missing setter, a non-public getter or setter, and an init-only setter, in that order. It then moves to the next property in reflection order.

DetailTypical cause
field X has an anonymous typeA public field whose type is anonymous.
property X has an anonymous typeAn anonymous type with a property whose type is also anonymous. An anonymous type with no such property passes the check without applying the remaining rules.
the type is not publicGenerated code must name the type, so it must be visible outside its assembly.
field X is not publicA non-public instance field other than an auto-property’s backing field. An F# record stops here, because it keeps its state in fields such as Key@.
field X is read-onlyA readonly field, as in a readonly struct.
property X has no setterA get-only auto-property.
property X is not publicAn auto-property whose getter or setter is not public.
property X has an init-only setterA { get; init; } property. A C# positional record stops here.
the type is abstract or an interfaceA class that is abstract, or an interface.
the type has no public parameterless constructorA class that Trill cannot create.

A struct skips the last two rules. For types other than anonymous types, columnar mode requires a public type that keeps its state in public, settable fields and auto-properties. A class must also be concrete and have a public parameterless constructor.

When the hook is called

  • On the thread that builds or subscribes to the query, at the moment the operator decides. It is never called while data flows.
  • Once for each fallback decision, with no deduplication. A query built twice reports twice. Map-reduce builds its query in its constructor and again at Subscribe, and each build reports. Multicast builds its selector once to work out the stream’s properties, and that build reports too.
  • It is process-wide: it sees reports from every query, and it may be called on several threads at once.
  • Not at all when Config.ForceRowBasedExecution is set, and not for a code generation failure that throws because fallback to row mode is disabled.
  • If the hook throws, the exception is caught and counted in Config.RowModeReporterFailures, and the query is built as usual.

Operators that convert between rows and columns on purpose, such as Union, do not report. A binary operator also does not report when it converts one input to rows because the other input is already in rows. The operator upstream that fell back has already reported.

See also: Reading the diagrams