From a757ecd8d57f3d32210d9fb313e190ecd93a846f Mon Sep 17 00:00:00 2001
From: Markus Stange <markus.stange@gmail.com>
Date: Fri, 31 Jan 2025 17:04:23 -0500
Subject: [PATCH] Make StaticSchemaMarker interface more ergonomic.

---
 fxprof-processed-profile/src/lib.rs           |   7 +-
 fxprof-processed-profile/src/markers.rs       | 660 +++++++++++-------
 fxprof-processed-profile/src/profile.rs       |  46 +-
 .../tests/integration_tests/main.rs           | 111 ++-
 samply/src/linux_shared/converter.rs          |  28 +-
 samply/src/shared/jit_function_add_marker.rs  |  37 +-
 samply/src/shared/per_cpu.rs                  |  77 +-
 samply/src/shared/process_sample_data.rs      | 193 ++---
 samply/src/windows/coreclr.rs                 | 132 ++--
 samply/src/windows/profile_context.rs         |  54 +-
 10 files changed, 688 insertions(+), 657 deletions(-)

diff --git a/fxprof-processed-profile/src/lib.rs b/fxprof-processed-profile/src/lib.rs
index 20b01119..729b21a1 100644
--- a/fxprof-processed-profile/src/lib.rs
+++ b/fxprof-processed-profile/src/lib.rs
@@ -68,9 +68,10 @@ pub use global_lib_table::{LibraryHandle, UsedLibraryAddressesIterator};
 pub use lib_mappings::LibMappings;
 pub use library_info::{LibraryInfo, Symbol, SymbolTable};
 pub use markers::{
-    GraphColor, Marker, MarkerFieldFormat, MarkerFieldFormatKind, MarkerFieldSchema,
-    MarkerGraphSchema, MarkerGraphType, MarkerHandle, MarkerLocation, MarkerSchema,
-    MarkerStaticField, MarkerTiming, MarkerTypeHandle, StaticSchemaMarker,
+    GraphColor, Marker, MarkerFieldFlags, MarkerFieldFormat, MarkerFieldFormatKind,
+    MarkerGraphType, MarkerHandle, MarkerLocations, MarkerTiming, MarkerTypeHandle,
+    RuntimeSchemaMarkerField, RuntimeSchemaMarkerGraph, RuntimeSchemaMarkerSchema,
+    StaticSchemaMarker, StaticSchemaMarkerField, StaticSchemaMarkerGraph,
 };
 pub use process::ThreadHandle;
 pub use profile::{FrameHandle, Profile, SamplingInterval, StackHandle, StringHandle};
diff --git a/fxprof-processed-profile/src/markers.rs b/fxprof-processed-profile/src/markers.rs
index 3f13218f..4904309f 100644
--- a/fxprof-processed-profile/src/markers.rs
+++ b/fxprof-processed-profile/src/markers.rs
@@ -2,6 +2,7 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+use bitflags::bitflags;
 use serde::ser::{SerializeMap, SerializeSeq};
 use serde::Serialize;
 use serde_derive::Serialize;
@@ -46,7 +47,7 @@ pub enum MarkerTiming {
 /// The marker trait. You'll likely want to implement [`StaticSchemaMarker`] instead.
 ///
 /// Markers have a type, a name, a category, and an arbitrary number of fields.
-/// The fields of a marker type are defined by the marker type's schema, see [`MarkerSchema`].
+/// The fields of a marker type are defined by the marker type's schema, see [`RuntimeSchemaMarkerSchema`].
 /// The timestamps are not part of the marker; they are supplied separately to
 /// [`Profile::add_marker`] when a marker is added to the profile.
 ///
@@ -68,30 +69,30 @@ pub trait Marker {
     /// The category of this marker. The marker chart groups marker rows by category.
     fn category(&self, profile: &mut Profile) -> CategoryHandle;
 
-    /// Called for any fields defined in the schema whose [`format`](MarkerFieldSchema::format) is
+    /// Called for any fields defined in the schema whose [`format`](RuntimeSchemaMarkerField::format) is
     /// of [kind](MarkerFieldFormat::kind) [`MarkerFieldFormatKind::String`].
     ///
-    /// `field_index` is an index into the schema's [`fields`](MarkerSchema::fields).
+    /// `field_index` is an index into the schema's [`fields`](RuntimeSchemaMarkerSchema::fields).
     ///
     /// You can panic for any unexpected field indexes, for example
     /// using `unreachable!()`. You can even panic unconditionally if this
     /// marker type doesn't have any string fields.
     ///
     /// If you do see unexpected calls to this method, make sure you're not registering
-    /// multiple different schemas with the same [`MarkerSchema::type_name`].
+    /// multiple different schemas with the same [`RuntimeSchemaMarkerSchema::type_name`].
     fn string_field_value(&self, field_index: u32) -> StringHandle;
 
-    /// Called for any fields defined in the schema whose [`format`](MarkerFieldSchema::format) is
+    /// Called for any fields defined in the schema whose [`format`](RuntimeSchemaMarkerField::format) is
     /// of [kind](MarkerFieldFormat::kind) [`MarkerFieldFormatKind::Number`].
     ///
-    /// `field_index` is an index into the schema's [`fields`](MarkerSchema::fields).
+    /// `field_index` is an index into the schema's [`fields`](RuntimeSchemaMarkerSchema::fields).
     ///
     /// You can panic for any unexpected field indexes, for example
     /// using `unreachable!()`. You can even panic unconditionally if this
     /// marker type doesn't have any number fields.
     ///
     /// If you do see unexpected calls to this method, make sure you're not registering
-    /// multiple different schemas with the same [`MarkerSchema::type_name`].
+    /// multiple different schemas with the same [`RuntimeSchemaMarkerSchema::type_name`].
     fn number_field_value(&self, field_index: u32) -> f64;
 }
 
@@ -99,7 +100,7 @@ pub trait Marker {
 /// [`StaticSchemaMarker`] automatically implements the [`Marker`] trait via a blanket impl.
 ///
 /// Markers have a type, a name, a category, and an arbitrary number of fields.
-/// The fields of a marker type are defined by the marker type's schema, see [`MarkerSchema`].
+/// The fields of a marker type are defined by the marker type's schema, see [`RuntimeSchemaMarkerSchema`].
 /// The timestamps are not part of the marker; they are supplied separately to
 /// [`Profile::add_marker`] when a marker is added to the profile.
 ///
@@ -107,7 +108,7 @@ pub trait Marker {
 ///
 /// ```
 /// use fxprof_processed_profile::{
-///     Profile, Marker, MarkerLocation, MarkerFieldFormat, MarkerSchema, MarkerFieldSchema,
+///     Profile, Marker, MarkerLocations, MarkerFieldFlags, MarkerFieldFormat, StaticSchemaMarkerField,
 ///     StaticSchemaMarker, CategoryHandle, StringHandle,
 /// };
 ///
@@ -121,23 +122,16 @@ pub trait Marker {
 /// impl StaticSchemaMarker for TextMarker {
 ///     const UNIQUE_MARKER_TYPE_NAME: &'static str = "Text";
 ///
-///     fn schema() -> MarkerSchema {
-///         MarkerSchema {
-///             type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-///             locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-///             chart_label: Some("{marker.data.text}".into()),
-///             tooltip_label: None,
-///             table_label: Some("{marker.name} - {marker.data.text}".into()),
-///             fields: vec![MarkerFieldSchema {
-///                 key: "text".into(),
-///                 label: "Contents".into(),
-///                 format: MarkerFieldFormat::String,
-///                 searchable: true,
-///             }],
-///             static_fields: vec![],
-///             graphs: vec![],
-///         }
-///     }
+///     const LOCATIONS: MarkerLocations = MarkerLocations::MARKER_CHART.union(MarkerLocations::MARKER_TABLE);
+///     const CHART_LABEL: Option<&'static str> = Some("{marker.data.text}");
+///     const TABLE_LABEL: Option<&'static str> = Some("{marker.name} - {marker.data.text}");
+///
+///     const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+///         key: "text",
+///         label: "Contents",
+///         format: MarkerFieldFormat::String,
+///         flags: MarkerFieldFlags::SEARCHABLE,
+///     }];
 ///
 ///     fn name(&self, _profile: &mut Profile) -> StringHandle {
 ///         self.name
@@ -158,11 +152,52 @@ pub trait Marker {
 /// ```
 pub trait StaticSchemaMarker {
     /// A unique string name for this marker type. Has to match the
-    /// [`MarkerSchema::type_name`] of this type's schema.
+    /// [`RuntimeSchemaMarkerSchema::type_name`] of this type's schema.
     const UNIQUE_MARKER_TYPE_NAME: &'static str;
 
-    /// The [`MarkerSchema`] for this marker type.
-    fn schema() -> MarkerSchema;
+    /// An optional description string. Applies to all markers of this type.
+    const DESCRIPTION: Option<&'static str> = None;
+
+    /// Set of marker display locations.
+    const LOCATIONS: MarkerLocations =
+        MarkerLocations::MARKER_CHART.union(MarkerLocations::MARKER_TABLE);
+
+    /// A template string defining the label shown within each marker's box in the marker chart.
+    ///
+    /// Usable template literals are `{marker.name}` and `{marker.data.fieldkey}`.
+    ///
+    /// If set to `None`, the boxes in the marker chart will be empty.
+    const CHART_LABEL: Option<&'static str> = None;
+
+    /// A template string defining the label shown in the first row of the marker's tooltip.
+    ///
+    /// Usable template literals are `{marker.name}` and `{marker.data.fieldkey}`.
+    ///
+    /// Defaults to `{marker.name}` if set to `None`.
+    const TOOLTIP_LABEL: Option<&'static str> = None;
+
+    /// A template string defining the label shown within each marker's box in the marker chart.
+    ///
+    /// Usable template literals are `{marker.name}` and `{marker.data.fieldkey}`.
+    ///
+    /// Defaults to `{marker.name}` if set to `None`.
+    const TABLE_LABEL: Option<&'static str> = None;
+
+    /// The marker fields. The values are supplied by each marker, in the marker's
+    /// implementations of the `string_field_value` and `number_field_value` trait methods.
+    const FIELDS: &'static [StaticSchemaMarkerField];
+
+    /// Any graph lines / segments created from markers of this type.
+    ///
+    /// If this is non-empty, the Firefox Profiler will create one graph track per
+    /// marker *name*, per thread, based on the markers it finds on that thread.
+    /// The marker name becomes the track's label.
+    ///
+    /// The elements in the graphs array describe individual graph lines or bar
+    /// chart segments which are all drawn inside the same track, stacked on top of
+    /// each other, in the order that they're listed here, with the first entry
+    /// becoming the bottom-most graph within the track.
+    const GRAPHS: &'static [StaticSchemaMarkerGraph] = &[];
 
     /// The name of this marker, as an interned string handle.
     ///
@@ -173,30 +208,30 @@ pub trait StaticSchemaMarker {
     /// The category of this marker. The marker chart groups marker rows by category.
     fn category(&self, profile: &mut Profile) -> CategoryHandle;
 
-    /// Called for any fields defined in the schema whose [`format`](MarkerFieldSchema::format) is
+    /// Called for any fields defined in the schema whose [`format`](RuntimeSchemaMarkerField::format) is
     /// of [kind](MarkerFieldFormat::kind) [`MarkerFieldFormatKind::String`].
     ///
-    /// `field_index` is an index into the schema's [`fields`](MarkerSchema::fields).
+    /// `field_index` is an index into the schema's [`fields`](RuntimeSchemaMarkerSchema::fields).
     ///
     /// You can panic for any unexpected field indexes, for example
     /// using `unreachable!()`. You can even panic unconditionally if this
     /// marker type doesn't have any string fields.
     ///
     /// If you do see unexpected calls to this method, make sure you're not registering
-    /// multiple different schemas with the same [`MarkerSchema::type_name`].
+    /// multiple different schemas with the same [`RuntimeSchemaMarkerSchema::type_name`].
     fn string_field_value(&self, field_index: u32) -> StringHandle;
 
-    /// Called for any fields defined in the schema whose [`format`](MarkerFieldSchema::format) is
+    /// Called for any fields defined in the schema whose [`format`](RuntimeSchemaMarkerField::format) is
     /// of [kind](MarkerFieldFormat::kind) [`MarkerFieldFormatKind::Number`].
     ///
-    /// `field_index` is an index into the schema's [`fields`](MarkerSchema::fields).
+    /// `field_index` is an index into the schema's [`fields`](RuntimeSchemaMarkerSchema::fields).
     ///
     /// You can panic for any unexpected field indexes, for example
     /// using `unreachable!()`. You can even panic unconditionally if this
     /// marker type doesn't have any number fields.
     ///
     /// If you do see unexpected calls to this method, make sure you're not registering
-    /// multiple different schemas with the same [`MarkerSchema::type_name`].
+    /// multiple different schemas with the same [`RuntimeSchemaMarkerSchema::type_name`].
     fn number_field_value(&self, field_index: u32) -> f64;
 }
 
@@ -223,282 +258,184 @@ impl<T: StaticSchemaMarker> Marker for T {
 }
 
 /// Describes a marker type, including the names and types of the marker's fields.
+/// You only need this if you don't know the schema until runtime. Otherwise, use
+/// [`StaticSchemaMarker`] instead.
 ///
 /// Example:
 ///
 /// ```
 /// use fxprof_processed_profile::{
-///     Profile, Marker, MarkerLocation, MarkerFieldFormat, MarkerSchema, MarkerFieldSchema,
-///     MarkerStaticField, StaticSchemaMarker, CategoryHandle, StringHandle,
+///     Profile, Marker, MarkerLocations, MarkerFieldFlags, MarkerFieldFormat, RuntimeSchemaMarkerSchema, RuntimeSchemaMarkerField,
+///     CategoryHandle, StringHandle,
 /// };
 ///
 /// # fn fun() {
-/// let schema = MarkerSchema {
+/// let schema = RuntimeSchemaMarkerSchema {
 ///     type_name: "custom".into(),
-///     locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
+///     locations: MarkerLocations::MARKER_CHART | MarkerLocations::MARKER_TABLE,
 ///     chart_label: Some("{marker.data.eventName}".into()),
 ///     tooltip_label: Some("Custom {marker.name} marker".into()),
 ///     table_label: Some("{marker.name} - {marker.data.eventName} with allocation size {marker.data.allocationSize} (latency: {marker.data.latency})".into()),
 ///     fields: vec![
-///         MarkerFieldSchema {
+///         RuntimeSchemaMarkerField {
 ///             key: "eventName".into(),
 ///             label: "Event name".into(),
 ///             format: MarkerFieldFormat::String,
-///             searchable: true,
+///             flags: MarkerFieldFlags::SEARCHABLE,
 ///         },
-///         MarkerFieldSchema {
+///         RuntimeSchemaMarkerField {
 ///             key: "allocationSize".into(),
 ///             label: "Allocation size".into(),
 ///             format: MarkerFieldFormat::Bytes,
-///             searchable: true,
+///             flags: MarkerFieldFlags::SEARCHABLE,
 ///         },
-///         MarkerFieldSchema {
+///         RuntimeSchemaMarkerField {
 ///             key: "url".into(),
 ///             label: "URL".into(),
 ///             format: MarkerFieldFormat::Url,
-///             searchable: true,
+///             flags: MarkerFieldFlags::SEARCHABLE,
 ///         },
-///         MarkerFieldSchema {
+///         RuntimeSchemaMarkerField {
 ///             key: "latency".into(),
 ///             label: "Latency".into(),
 ///             format: MarkerFieldFormat::Duration,
-///             searchable: true,
+///             flags: MarkerFieldFlags::SEARCHABLE,
 ///         },
 ///     ],
-///     static_fields: vec![MarkerStaticField {
-///         label: "Description".into(),
-///         value: "This is a test marker with a custom schema.".into(),
-///     }],
+///     description: Some("This is a test marker with a custom schema.".into()),
 ///     graphs: vec![],
 /// };
 /// # }
 /// ```
 #[derive(Debug, Clone)]
-pub struct MarkerSchema {
+pub struct RuntimeSchemaMarkerSchema {
     /// The unique name of this marker type. There must not be any other schema
     /// with the same name.
     pub type_name: String,
 
-    /// List of marker display locations.
-    pub locations: Vec<MarkerLocation>,
+    /// An optional description string. Applies to all markers of this type.
+    pub description: Option<String>,
+
+    /// Set of marker display locations.
+    pub locations: MarkerLocations,
 
     /// A template string defining the label shown within each marker's box in the marker chart.
     ///
-    /// Usable template literals are `{marker.name}` and `{marker.data.fieldname}`.
+    /// Usable template literals are `{marker.name}` and `{marker.data.fieldkey}`.
     ///
     /// If set to `None`, the boxes in the marker chart will be empty.
     pub chart_label: Option<String>,
 
     /// A template string defining the label shown in the first row of the marker's tooltip.
     ///
-    /// Usable template literals are `{marker.name}` and `{marker.data.fieldname}`.
+    /// Usable template literals are `{marker.name}` and `{marker.data.fieldkey}`.
     ///
-    /// Defaults to `{marker.name}` if set to `None`. (TODO: verify this is true)
+    /// Defaults to `{marker.name}` if set to `None`.
     pub tooltip_label: Option<String>,
 
     /// A template string defining the label shown within each marker's box in the marker chart.
     ///
-    /// Usable template literals are `{marker.name}` and `{marker.data.fieldname}`.
+    /// Usable template literals are `{marker.name}` and `{marker.data.fieldkey}`.
     ///
-    /// Defaults to `{marker.name}` if set to `None`. (TODO: verify this is true)
+    /// Defaults to `{marker.name}` if set to `None`.
     pub table_label: Option<String>,
 
     /// The marker fields. The values are supplied by each marker, in the marker's
     /// implementations of the `string_field_value` and `number_field_value` trait methods.
-    pub fields: Vec<MarkerFieldSchema>,
-
-    /// The static fields of this marker type, with fixed values that apply to all markers of this type.
-    /// These are usually used for things like a human readable marker type description.
-    pub static_fields: Vec<MarkerStaticField>,
+    pub fields: Vec<RuntimeSchemaMarkerField>,
 
     /// Any graph lines / segments created from markers of this type.
     ///
     /// If this is non-empty, the Firefox Profiler will create one graph track per
-    /// marker *name*, per thread, based on the markers it sees on that thread.
+    /// marker *name*, per thread, based on the markers it finds on that thread.
     /// The marker name becomes the track's label.
     ///
     /// The elements in the graphs array describe individual graph lines or bar
     /// chart segments which are all drawn inside the same track, stacked on top of
     /// each other, in the order that they're listed here, with the first entry
-    /// becoming the bottom-most graph segment within the track.
-    pub graphs: Vec<MarkerGraphSchema>,
+    /// becoming the bottom-most graph within the track.
+    pub graphs: Vec<RuntimeSchemaMarkerGraph>,
 }
 
-#[derive(Debug, Clone)]
-pub struct InternalMarkerSchema {
-    /// The name of this marker type.
-    type_name: String,
-
-    /// List of marker display locations.
-    locations: Vec<MarkerLocation>,
-
-    chart_label: Option<String>,
-    tooltip_label: Option<String>,
-    table_label: Option<String>,
-
-    /// The marker fields. These can be specified on each marker.
-    fields: Vec<MarkerFieldSchema>,
-
-    /// Any graph tracks created from markers of this type
-    graphs: Vec<MarkerGraphSchema>,
-
-    string_field_count: usize,
-    number_field_count: usize,
-
-    /// The static fields of this marker type, with fixed values that apply to all markers.
-    /// These are usually used for things like a human readable marker type description.
-    static_fields: Vec<MarkerStaticField>,
-}
-
-impl From<MarkerSchema> for InternalMarkerSchema {
-    fn from(schema: MarkerSchema) -> Self {
-        let string_field_count = schema
-            .fields
-            .iter()
-            .filter(|f| f.format.kind() == MarkerFieldFormatKind::String)
-            .count();
-        let number_field_count = schema
-            .fields
-            .iter()
-            .filter(|f| f.format.kind() == MarkerFieldFormatKind::Number)
-            .count();
-        Self {
-            type_name: schema.type_name,
-            locations: schema.locations,
-            chart_label: schema.chart_label,
-            tooltip_label: schema.tooltip_label,
-            table_label: schema.table_label,
-            fields: schema.fields,
-            graphs: schema.graphs,
-            string_field_count,
-            number_field_count,
-            static_fields: schema.static_fields,
-        }
+bitflags! {
+    /// Locations in the profiler UI where markers can be displayed.
+    #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
+    pub struct MarkerLocations: u32 {
+        /// Show the marker in the "marker chart" panel.
+        const MARKER_CHART = 1 << 0;
+        /// Show the marker in the marker table.
+        const MARKER_TABLE = 1 << 1;
+        /// This adds markers to the main marker timeline in the header, but only
+        /// for main threads and for threads that were specifically asked to show
+        /// these markers using [`Profile::set_thread_show_markers_in_timeline`].
+        const TIMELINE_OVERVIEW = 1 << 2;
+        /// In the timeline, this is a section that breaks out markers that are
+        /// related to memory. When memory counters are used, this is its own
+        /// track, otherwise it is displayed with the main thread.
+        const TIMELINE_MEMORY = 1 << 3;
+        /// This adds markers to the IPC timeline area in the header.
+        const TIMELINE_IPC = 1 << 4;
+        /// This adds markers to the FileIO timeline area in the header.
+        const TIMELINE_FILEIO = 1 << 5;
     }
 }
 
-impl InternalMarkerSchema {
-    pub fn type_name(&self) -> &str {
-        &self.type_name
-    }
-    pub fn fields(&self) -> &[MarkerFieldSchema] {
-        &self.fields
-    }
-    pub fn string_field_count(&self) -> usize {
-        self.string_field_count
-    }
-    pub fn number_field_count(&self) -> usize {
-        self.number_field_count
-    }
-    fn serialize_self<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        let mut map = serializer.serialize_map(None)?;
-        map.serialize_entry("name", &self.type_name)?;
-        map.serialize_entry("display", &self.locations)?;
-        if let Some(label) = &self.chart_label {
-            map.serialize_entry("chartLabel", label)?;
-        }
-        if let Some(label) = &self.tooltip_label {
-            map.serialize_entry("tooltipLabel", label)?;
-        }
-        if let Some(label) = &self.table_label {
-            map.serialize_entry("tableLabel", label)?;
-        }
-        map.serialize_entry("data", &SerializableSchemaFields(self))?;
-        if !self.graphs.is_empty() {
-            map.serialize_entry("graphs", &self.graphs)?;
-        }
-        map.end()
-    }
-
-    fn serialize_fields<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        let mut seq =
-            serializer.serialize_seq(Some(self.fields.len() + self.static_fields.len()))?;
-        for field in &self.fields {
-            seq.serialize_element(field)?;
-        }
-        for field in &self.static_fields {
-            seq.serialize_element(field)?;
-        }
-        seq.end()
-    }
-}
+/// The field definition of a marker field, used in [`StaticSchemaMarker::FIELDS`].
+///
+/// For each marker which uses this schema, the value for this field is supplied by the
+/// marker's implementation of [`number_field_value`](Marker::number_field_value) /
+/// [`string_field_value`](Marker::string_field_value), depending on this field
+/// format's [kind](MarkerFieldFormat::kind).
+///
+/// Used with runtime-generated marker schemas. Use [`RuntimeSchemaMarkerField`]
+/// when using [`RuntimeSchemaMarkerSchema`].
+pub struct StaticSchemaMarkerField {
+    /// The field key. Must not be `type` or `cause`.
+    pub key: &'static str,
 
-impl Serialize for InternalMarkerSchema {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        self.serialize_self(serializer)
-    }
-}
+    /// The user-visible label of this field.
+    pub label: &'static str,
 
-struct SerializableSchemaFields<'a>(&'a InternalMarkerSchema);
+    /// The format of this field.
+    pub format: MarkerFieldFormat,
 
-impl Serialize for SerializableSchemaFields<'_> {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        self.0.serialize_fields(serializer)
-    }
+    /// Additional field flags.
+    pub flags: MarkerFieldFlags,
 }
 
-// /// The location of markers with this type.
+/// The field definition of a marker field, used in [`RuntimeSchemaMarkerSchema::fields`].
 ///
-/// Markers can be shown in different parts of the Firefox Profiler UI.
+/// For each marker which uses this schema, the value for this field is supplied by the
+/// marker's implementation of [`number_field_value`](Marker::number_field_value) /
+/// [`string_field_value`](Marker::string_field_value), depending on this field
+/// format's [kind](MarkerFieldFormat::kind).
 ///
-/// Multiple [`MarkerLocation`]s can be specified for a single marker type.
-#[derive(Debug, Clone, Serialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum MarkerLocation {
-    MarkerChart,
-    MarkerTable,
-    /// This adds markers to the main marker timeline in the header, but only
-    /// for main threads and for threads that were specifically asked to show
-    /// these markers using [`Profile::set_thread_show_markers_in_timeline`].
-    TimelineOverview,
-    /// In the timeline, this is a section that breaks out markers that are
-    /// related to memory. When memory counters are enabled, this is its own
-    /// track, otherwise it is displayed with the main thread.
-    TimelineMemory,
-    /// This adds markers to the IPC timeline area in the header.
-    TimelineIPC,
-    /// This adds markers to the FileIO timeline area in the header.
-    #[serde(rename = "timeline-fileio")]
-    TimelineFileIO,
-    /// TODO - This is not supported yet.
-    StackChart,
-}
-
-/// The field description of a marker field which has the same key and value on all markers with this schema.
-#[derive(Debug, Clone, Serialize)]
-pub struct MarkerStaticField {
-    pub label: String,
-    pub value: String,
-}
-
-/// The field description of a marker field. The value for this field is supplied by the marker's implementation
-/// of [`number_field_value`](Marker::number_field_value) / [`string_field_value`](Marker::string_field_value).
-#[derive(Debug, Clone, Serialize)]
-pub struct MarkerFieldSchema {
+/// Used with runtime-generated marker schemas. Use [`StaticSchemaMarkerField`]
+/// when using [`StaticSchemaMarker`].
+#[derive(Debug, Clone)]
+pub struct RuntimeSchemaMarkerField {
     /// The field key. Must not be `type` or `cause`.
     pub key: String,
 
     /// The user-visible label of this field.
-    #[serde(skip_serializing_if = "str::is_empty")]
     pub label: String,
 
     /// The format of this field.
     pub format: MarkerFieldFormat,
 
     /// Whether this field's value should be matched against search terms.
-    pub searchable: bool,
+    pub flags: MarkerFieldFlags,
+}
+
+impl From<&StaticSchemaMarkerField> for RuntimeSchemaMarkerField {
+    fn from(schema: &StaticSchemaMarkerField) -> Self {
+        Self {
+            key: schema.key.into(),
+            label: schema.label.into(),
+            format: schema.format.clone(),
+            flags: schema.flags,
+        }
+    }
 }
 
 /// The field format of a marker field.
@@ -613,6 +550,60 @@ impl MarkerFieldFormat {
     }
 }
 
+bitflags! {
+    /// Marker field flags, used in the marker schema.
+    #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
+    pub struct MarkerFieldFlags: u32 {
+        /// Whether this field's value should be matched against search terms.
+        const SEARCHABLE = 0b00000001;
+    }
+}
+
+/// A graph within a marker graph track, used in [`StaticSchemaMarker::GRAPHS`].
+///
+/// Used with runtime-generated marker schemas. Use [`RuntimeSchemaMarkerGraph`]
+/// when using [`RuntimeSchemaMarkerSchema`].
+pub struct StaticSchemaMarkerGraph {
+    /// The key of a number field that's declared in the marker schema.
+    ///
+    /// The values of this field are the values of this graph line /
+    /// bar graph segment.
+    pub key: &'static str,
+    /// Whether this marker graph segment is a line or a bar graph segment.
+    pub graph_type: MarkerGraphType,
+    /// The color of the graph segment. If `None`, the choice is up to the front-end.
+    pub color: Option<GraphColor>,
+}
+
+/// A graph within a marker graph track, used in [`RuntimeSchemaMarkerSchema::graphs`].
+///
+/// Used with runtime-generated marker schemas. Use [`StaticSchemaMarkerGraph`]
+/// when using [`StaticSchemaMarker`].
+#[derive(Clone, Debug, Serialize)]
+pub struct RuntimeSchemaMarkerGraph {
+    /// The key of a number field that's declared in the marker schema.
+    ///
+    /// The values of this field are the values of this graph line /
+    /// bar graph segment.
+    pub key: String,
+    /// Whether this marker graph segment is a line or a bar graph segment.
+    #[serde(rename = "type")]
+    pub graph_type: MarkerGraphType,
+    /// The color of the graph segment. If `None`, the choice is up to the front-end.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub color: Option<GraphColor>,
+}
+
+impl From<&StaticSchemaMarkerGraph> for RuntimeSchemaMarkerGraph {
+    fn from(schema: &StaticSchemaMarkerGraph) -> Self {
+        Self {
+            key: schema.key.into(),
+            graph_type: schema.graph_type,
+            color: schema.color,
+        }
+    }
+}
+
 /// The type of a graph segment within a marker graph.
 #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize)]
 #[serde(rename_all = "kebab-case")]
@@ -641,18 +632,215 @@ pub enum GraphColor {
     Yellow,
 }
 
-/// One segment within a marker graph track.
-#[derive(Clone, Debug, Serialize)]
-pub struct MarkerGraphSchema {
-    /// The key of a number field that's declared in the marker schema.
-    ///
-    /// The values of this field are the values of this graph line /
-    /// bar graph segment.
-    pub key: &'static str,
-    /// Whether this marker graph segment is a line or a bar graph segment.
-    #[serde(rename = "type")]
-    pub graph_type: MarkerGraphType,
-    /// The color of the graph segment. If `None`, the choice is up to the front-end.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub color: Option<GraphColor>,
+#[derive(Debug, Clone)]
+pub struct InternalMarkerSchema {
+    /// The name of this marker type.
+    type_name: String,
+
+    /// List of marker display locations.
+    locations: MarkerLocations,
+
+    chart_label: Option<String>,
+    tooltip_label: Option<String>,
+    table_label: Option<String>,
+
+    /// The marker fields. These can be specified on each marker.
+    fields: Vec<RuntimeSchemaMarkerField>,
+
+    /// Any graph tracks created from markers of this type
+    graphs: Vec<RuntimeSchemaMarkerGraph>,
+
+    string_field_count: usize,
+    number_field_count: usize,
+
+    description: Option<String>,
+}
+
+impl From<RuntimeSchemaMarkerSchema> for InternalMarkerSchema {
+    fn from(schema: RuntimeSchemaMarkerSchema) -> Self {
+        Self::from_runtime_schema(schema)
+    }
+}
+
+impl InternalMarkerSchema {
+    pub fn from_runtime_schema(schema: RuntimeSchemaMarkerSchema) -> Self {
+        let string_field_count = schema
+            .fields
+            .iter()
+            .filter(|f| f.format.kind() == MarkerFieldFormatKind::String)
+            .count();
+        let number_field_count = schema
+            .fields
+            .iter()
+            .filter(|f| f.format.kind() == MarkerFieldFormatKind::Number)
+            .count();
+        Self {
+            type_name: schema.type_name,
+            locations: schema.locations,
+            chart_label: schema.chart_label,
+            tooltip_label: schema.tooltip_label,
+            table_label: schema.table_label,
+            fields: schema.fields,
+            graphs: schema.graphs,
+            string_field_count,
+            number_field_count,
+            description: schema.description,
+        }
+    }
+
+    pub fn from_static_schema<T: StaticSchemaMarker>() -> Self {
+        let string_field_count = T::FIELDS
+            .iter()
+            .filter(|f| f.format.kind() == MarkerFieldFormatKind::String)
+            .count();
+        let number_field_count = T::FIELDS
+            .iter()
+            .filter(|f| f.format.kind() == MarkerFieldFormatKind::Number)
+            .count();
+        Self {
+            type_name: T::UNIQUE_MARKER_TYPE_NAME.into(),
+            locations: T::LOCATIONS,
+            chart_label: T::CHART_LABEL.map(Into::into),
+            tooltip_label: T::TOOLTIP_LABEL.map(Into::into),
+            table_label: T::TABLE_LABEL.map(Into::into),
+            fields: T::FIELDS.iter().map(Into::into).collect(),
+            string_field_count,
+            number_field_count,
+            description: T::DESCRIPTION.map(Into::into),
+            graphs: T::GRAPHS.iter().map(Into::into).collect(),
+        }
+    }
+
+    pub fn type_name(&self) -> &str {
+        &self.type_name
+    }
+    pub fn fields(&self) -> &[RuntimeSchemaMarkerField] {
+        &self.fields
+    }
+    pub fn string_field_count(&self) -> usize {
+        self.string_field_count
+    }
+    pub fn number_field_count(&self) -> usize {
+        self.number_field_count
+    }
+    fn serialize_self<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let mut map = serializer.serialize_map(None)?;
+        map.serialize_entry("name", &self.type_name)?;
+        map.serialize_entry("display", &SerializableSchemaDisplay(self.locations))?;
+        if let Some(label) = &self.chart_label {
+            map.serialize_entry("chartLabel", label)?;
+        }
+        if let Some(label) = &self.tooltip_label {
+            map.serialize_entry("tooltipLabel", label)?;
+        }
+        if let Some(label) = &self.table_label {
+            map.serialize_entry("tableLabel", label)?;
+        }
+        map.serialize_entry("data", &SerializableSchemaFields(self))?;
+        if !self.graphs.is_empty() {
+            map.serialize_entry("graphs", &self.graphs)?;
+        }
+        map.end()
+    }
+
+    fn serialize_fields<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let mut seq = serializer.serialize_seq(None)?;
+        for field in &self.fields {
+            seq.serialize_element(&SerializableSchemaField(field))?;
+        }
+        if let Some(description) = &self.description {
+            seq.serialize_element(&SerializableDescriptionStaticField(description))?;
+        }
+        seq.end()
+    }
+}
+
+impl Serialize for InternalMarkerSchema {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        self.serialize_self(serializer)
+    }
+}
+
+struct SerializableSchemaFields<'a>(&'a InternalMarkerSchema);
+
+impl Serialize for SerializableSchemaFields<'_> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        self.0.serialize_fields(serializer)
+    }
+}
+
+struct SerializableSchemaField<'a>(&'a RuntimeSchemaMarkerField);
+
+impl Serialize for SerializableSchemaField<'_> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let mut map = serializer.serialize_map(None)?;
+        map.serialize_entry("key", &self.0.key)?;
+        if !self.0.label.is_empty() {
+            map.serialize_entry("label", &self.0.label)?;
+        }
+        map.serialize_entry("format", &self.0.format)?;
+        if self.0.flags.contains(MarkerFieldFlags::SEARCHABLE) {
+            map.serialize_entry("searchable", &true)?;
+        }
+        map.end()
+    }
+}
+
+struct SerializableDescriptionStaticField<'a>(&'a str);
+
+impl Serialize for SerializableDescriptionStaticField<'_> {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let mut map = serializer.serialize_map(None)?;
+        map.serialize_entry("label", "Description")?;
+        map.serialize_entry("value", self.0)?;
+        map.end()
+    }
+}
+
+struct SerializableSchemaDisplay(MarkerLocations);
+
+impl Serialize for SerializableSchemaDisplay {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        let mut seq = serializer.serialize_seq(None)?;
+        if self.0.contains(MarkerLocations::MARKER_CHART) {
+            seq.serialize_element("marker-chart")?;
+        }
+        if self.0.contains(MarkerLocations::MARKER_TABLE) {
+            seq.serialize_element("marker-table")?;
+        }
+        if self.0.contains(MarkerLocations::TIMELINE_OVERVIEW) {
+            seq.serialize_element("timeline-overview")?;
+        }
+        if self.0.contains(MarkerLocations::TIMELINE_MEMORY) {
+            seq.serialize_element("timeline-memory")?;
+        }
+        if self.0.contains(MarkerLocations::TIMELINE_IPC) {
+            seq.serialize_element("timeline-ipc")?;
+        }
+        if self.0.contains(MarkerLocations::TIMELINE_FILEIO) {
+            seq.serialize_element("timeline-fileio")?;
+        }
+        seq.end()
+    }
 }
diff --git a/fxprof-processed-profile/src/profile.rs b/fxprof-processed-profile/src/profile.rs
index 06b2fd89..7e71b20a 100644
--- a/fxprof-processed-profile/src/profile.rs
+++ b/fxprof-processed-profile/src/profile.rs
@@ -16,8 +16,8 @@ use crate::global_lib_table::{GlobalLibTable, LibraryHandle, UsedLibraryAddresse
 use crate::lib_mappings::LibMappings;
 use crate::library_info::{LibraryInfo, SymbolTable};
 use crate::markers::{
-    GraphColor, InternalMarkerSchema, Marker, MarkerHandle, MarkerSchema, MarkerTiming,
-    MarkerTypeHandle, StaticSchemaMarker,
+    GraphColor, InternalMarkerSchema, Marker, MarkerHandle, MarkerTiming, MarkerTypeHandle,
+    RuntimeSchemaMarkerSchema, StaticSchemaMarker,
 };
 use crate::process::{Process, ThreadHandle};
 use crate::reference_timestamp::ReferenceTimestamp;
@@ -423,7 +423,7 @@ impl Profile {
         self.threads[thread.0].set_tid(tid);
     }
 
-    /// Set whether to show a timeline view displaying [`MarkerLocation::TimelineOverview`](crate::MarkerLocation::TimelineOverview)
+    /// Set whether to show a timeline which displays [`MarkerLocations::TIMELINE_OVERVIEW`](crate::MarkerLocations::TIMELINE_OVERVIEW)
     /// markers for this thread.
     ///
     /// Main threads always have such a timeline view and always display such markers,
@@ -670,7 +670,7 @@ impl Profile {
         );
     }
 
-    /// Registers a marker type, given the type's [`MarkerSchema`]. Usually you only need to call this for
+    /// Registers a marker type for a [`RuntimeSchemaMarkerSchema`]. You only need to call this for
     /// marker types whose schema is dynamically created at runtime.
     ///
     /// After you register the marker type, you'll save its [`MarkerTypeHandle`] somewhere, and then
@@ -678,8 +678,8 @@ impl Profile {
     /// handle from its implementation of [`Marker::marker_type`].
     ///
     /// For marker types whose schema is known at compile time, you'll want to implement
-    /// [`StaticSchemaMarker`] instead.
-    pub fn register_marker_type(&mut self, schema: MarkerSchema) -> MarkerTypeHandle {
+    /// [`StaticSchemaMarker`] instead, and you don't need to call this method.
+    pub fn register_marker_type(&mut self, schema: RuntimeSchemaMarkerSchema) -> MarkerTypeHandle {
         let handle = MarkerTypeHandle(self.marker_schemas.len());
         self.marker_schemas.push(schema.into());
         handle
@@ -697,7 +697,8 @@ impl Profile {
             Entry::Occupied(entry) => *entry.get(),
             Entry::Vacant(entry) => {
                 let handle = MarkerTypeHandle(self.marker_schemas.len());
-                self.marker_schemas.push(T::schema().into());
+                let schema = InternalMarkerSchema::from_static_schema::<T>();
+                self.marker_schemas.push(schema);
                 entry.insert(handle);
                 handle
             }
@@ -710,9 +711,8 @@ impl Profile {
     ///
     /// ```
     /// use fxprof_processed_profile::{
-    ///     Profile, Marker, MarkerTiming, MarkerLocation, MarkerFieldFormat, MarkerSchema,
-    ///     MarkerFieldSchema, StaticSchemaMarker, CategoryHandle, StringHandle, ThreadHandle,
-    ///     Timestamp,
+    ///     Profile, CategoryHandle, Marker, MarkerFieldFlags, MarkerFieldFormat, MarkerTiming,
+    ///     StaticSchemaMarker, StaticSchemaMarkerField, StringHandle, ThreadHandle, Timestamp,
     /// };
     ///
     /// # fn fun() {
@@ -735,23 +735,15 @@ impl Profile {
     /// impl StaticSchemaMarker for TextMarker {
     ///     const UNIQUE_MARKER_TYPE_NAME: &'static str = "Text";
     ///
-    ///     fn schema() -> MarkerSchema {
-    ///         MarkerSchema {
-    ///             type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-    ///             locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-    ///             chart_label: Some("{marker.data.text}".into()),
-    ///             tooltip_label: None,
-    ///             table_label: Some("{marker.name} - {marker.data.text}".into()),
-    ///             fields: vec![MarkerFieldSchema {
-    ///                 key: "text".into(),
-    ///                 label: "Contents".into(),
-    ///                 format: MarkerFieldFormat::String,
-    ///                 searchable: true,
-    ///             }],
-    ///             static_fields: vec![],
-    ///             graphs: vec![],
-    ///         }
-    ///     }
+    ///     const CHART_LABEL: Option<&'static str> = Some("{marker.data.text}");
+    ///     const TABLE_LABEL: Option<&'static str> = Some("{marker.name} - {marker.data.text}");
+    ///
+    ///     const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+    ///         key: "text",
+    ///         label: "Contents",
+    ///         format: MarkerFieldFormat::String,
+    ///         flags: MarkerFieldFlags::SEARCHABLE,
+    ///     }];
     ///
     ///     fn name(&self, _profile: &mut Profile) -> StringHandle {
     ///         self.name
diff --git a/fxprof-processed-profile/tests/integration_tests/main.rs b/fxprof-processed-profile/tests/integration_tests/main.rs
index d91e8b7d..e2a84f9d 100644
--- a/fxprof-processed-profile/tests/integration_tests/main.rs
+++ b/fxprof-processed-profile/tests/integration_tests/main.rs
@@ -5,9 +5,9 @@ use assert_json_diff::assert_json_eq;
 use debugid::DebugId;
 use fxprof_processed_profile::{
     CategoryColor, CategoryHandle, CpuDelta, Frame, FrameFlags, FrameInfo, GraphColor, LibraryInfo,
-    MarkerFieldFormat, MarkerFieldSchema, MarkerGraphSchema, MarkerGraphType, MarkerLocation,
-    MarkerSchema, MarkerStaticField, MarkerTiming, Profile, ReferenceTimestamp, SamplingInterval,
-    StaticSchemaMarker, StringHandle, Symbol, SymbolTable, Timestamp, WeightType,
+    MarkerFieldFlags, MarkerFieldFormat, MarkerGraphType, MarkerTiming, Profile,
+    ReferenceTimestamp, SamplingInterval, StaticSchemaMarker, StaticSchemaMarkerField,
+    StaticSchemaMarkerGraph, StringHandle, Symbol, SymbolTable, Timestamp, WeightType,
 };
 use serde_json::json;
 
@@ -22,24 +22,14 @@ pub struct TextMarker {
 
 impl StaticSchemaMarker for TextMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "Text";
-
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.name}".into()),
-            tooltip_label: None,
-            table_label: Some("{marker.name} - {marker.data.name}".into()),
-            fields: vec![MarkerFieldSchema {
-                key: "name".into(),
-                label: "Details".into(),
-                format: MarkerFieldFormat::String,
-                searchable: true,
-            }],
-            static_fields: vec![],
-            graphs: vec![],
-        }
-    }
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.name}");
+    const TABLE_LABEL: Option<&'static str> = Some("{marker.name} - {marker.data.name}");
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "name",
+        label: "Details",
+        format: MarkerFieldFormat::String,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, _profile: &mut Profile) -> StringHandle {
         self.name
@@ -68,52 +58,43 @@ fn profile_without_js() {
     }
     impl StaticSchemaMarker for CustomMarker {
         const UNIQUE_MARKER_TYPE_NAME: &'static str = "custom";
+        const TOOLTIP_LABEL: Option<&'static str> = Some("Custom tooltip label");
 
-        fn schema() -> MarkerSchema {
-            MarkerSchema {
-                type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-                locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-                chart_label: None,
-                tooltip_label: Some("Custom tooltip label".into()),
-                table_label: None,
-                fields: vec![
-                    MarkerFieldSchema {
-                        key: "eventName".into(),
-                        label: "Event name".into(),
-                        format: MarkerFieldFormat::String,
-                        searchable: true,
-                    },
-                    MarkerFieldSchema {
-                        key: "allocationSize".into(),
-                        label: "Allocation size".into(),
-                        format: MarkerFieldFormat::Bytes,
-                        searchable: true,
-                    },
-                    MarkerFieldSchema {
-                        key: "url".into(),
-                        label: "URL".into(),
-                        format: MarkerFieldFormat::Url,
-                        searchable: true,
-                    },
-                    MarkerFieldSchema {
-                        key: "latency".into(),
-                        label: "Latency".into(),
-                        format: MarkerFieldFormat::Duration,
-                        searchable: true,
-                    },
-                ],
-                static_fields: vec![MarkerStaticField {
-                    label: "Description".into(),
-                    value: "This is a test marker with a custom schema.".into(),
-                }],
+        const FIELDS: &'static [StaticSchemaMarkerField] = &[
+            StaticSchemaMarkerField {
+                key: "eventName",
+                label: "Event name",
+                format: MarkerFieldFormat::String,
+                flags: MarkerFieldFlags::SEARCHABLE,
+            },
+            StaticSchemaMarkerField {
+                key: "allocationSize",
+                label: "Allocation size",
+                format: MarkerFieldFormat::Bytes,
+                flags: MarkerFieldFlags::SEARCHABLE,
+            },
+            StaticSchemaMarkerField {
+                key: "url",
+                label: "URL",
+                format: MarkerFieldFormat::Url,
+                flags: MarkerFieldFlags::SEARCHABLE,
+            },
+            StaticSchemaMarkerField {
+                key: "latency",
+                label: "Latency",
+                format: MarkerFieldFormat::Duration,
+                flags: MarkerFieldFlags::SEARCHABLE,
+            },
+        ];
 
-                graphs: vec![MarkerGraphSchema {
-                    key: "latency",
-                    graph_type: MarkerGraphType::Line,
-                    color: Some(GraphColor::Green),
-                }],
-            }
-        }
+        const DESCRIPTION: Option<&'static str> =
+            Some("This is a test marker with a custom schema.");
+
+        const GRAPHS: &'static [StaticSchemaMarkerGraph] = &[StaticSchemaMarkerGraph {
+            key: "latency",
+            graph_type: MarkerGraphType::Line,
+            color: Some(GraphColor::Green),
+        }];
 
         fn name(&self, profile: &mut Profile) -> StringHandle {
             profile.intern_string("CustomName")
diff --git a/samply/src/linux_shared/converter.rs b/samply/src/linux_shared/converter.rs
index 063808c2..3f8a15da 100644
--- a/samply/src/linux_shared/converter.rs
+++ b/samply/src/linux_shared/converter.rs
@@ -8,8 +8,8 @@ use debugid::DebugId;
 use framehop::{ExplicitModuleSectionInfo, FrameAddress, Module, Unwinder};
 use fxprof_processed_profile::{
     CategoryColor, CategoryHandle, CategoryPairHandle, CpuDelta, LibraryHandle, LibraryInfo,
-    MarkerFieldFormat, MarkerFieldSchema, MarkerLocation, MarkerSchema, MarkerTiming, Profile,
-    ReferenceTimestamp, SamplingInterval, StaticSchemaMarker, StringHandle, SymbolTable,
+    MarkerFieldFlags, MarkerFieldFormat, MarkerTiming, Profile, ReferenceTimestamp,
+    SamplingInterval, StaticSchemaMarker, StaticSchemaMarkerField, StringHandle, SymbolTable,
     ThreadHandle,
 };
 use linux_perf_data::linux_perf_event_reader::TaskWasPreempted;
@@ -1902,23 +1902,13 @@ struct MmapMarker(StringHandle);
 
 impl StaticSchemaMarker for MmapMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "mmap";
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.name}".into()),
-            tooltip_label: Some("{marker.name} - {marker.data.name}".into()),
-            table_label: Some("{marker.name} - {marker.data.name}".into()),
-            fields: vec![MarkerFieldSchema {
-                key: "name".into(),
-                label: "Details".into(),
-                format: MarkerFieldFormat::String,
-                searchable: true,
-            }],
-            static_fields: vec![],
-            graphs: vec![],
-        }
-    }
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "name",
+        label: "Details",
+        format: MarkerFieldFormat::String,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, profile: &mut Profile) -> StringHandle {
         profile.intern_string("mmap")
diff --git a/samply/src/shared/jit_function_add_marker.rs b/samply/src/shared/jit_function_add_marker.rs
index 2e56c424..95d4b27f 100644
--- a/samply/src/shared/jit_function_add_marker.rs
+++ b/samply/src/shared/jit_function_add_marker.rs
@@ -1,6 +1,6 @@
 use fxprof_processed_profile::{
-    CategoryHandle, MarkerFieldFormat, MarkerFieldSchema, MarkerLocation, MarkerSchema,
-    MarkerStaticField, Profile, StaticSchemaMarker, StringHandle,
+    CategoryHandle, MarkerFieldFlags, MarkerFieldFormat, Profile, StaticSchemaMarker,
+    StaticSchemaMarkerField, StringHandle,
 };
 
 #[derive(Debug, Clone)]
@@ -9,26 +9,19 @@ pub struct JitFunctionAddMarker(pub StringHandle);
 impl StaticSchemaMarker for JitFunctionAddMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "JitFunctionAdd";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.n}".into()),
-            tooltip_label: Some("{marker.data.n}".into()),
-            table_label: Some("{marker.data.n}".into()),
-            fields: vec![MarkerFieldSchema {
-                key: "n".into(),
-                label: "Function".into(),
-                format: MarkerFieldFormat::String,
-                searchable: true,
-            }],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value: "Emitted when a JIT function is added to the process.".into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const DESCRIPTION: Option<&'static str> =
+        Some("Emitted when a JIT function is added to the process.");
+
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.n}");
+    const TOOLTIP_LABEL: Option<&'static str> = Some("{marker.data.n}");
+    const TABLE_LABEL: Option<&'static str> = Some("{marker.data.n}");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "n",
+        label: "Function",
+        format: MarkerFieldFormat::String,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, profile: &mut Profile) -> StringHandle {
         profile.intern_string("JitFunctionAdd")
diff --git a/samply/src/shared/per_cpu.rs b/samply/src/shared/per_cpu.rs
index aa975ee7..a3ef3145 100644
--- a/samply/src/shared/per_cpu.rs
+++ b/samply/src/shared/per_cpu.rs
@@ -1,6 +1,6 @@
 use fxprof_processed_profile::{
-    CategoryHandle, Frame, FrameFlags, FrameInfo, MarkerFieldFormat, MarkerFieldSchema,
-    MarkerLocation, MarkerSchema, MarkerTiming, ProcessHandle, Profile, StaticSchemaMarker,
+    CategoryHandle, Frame, FrameFlags, FrameInfo, MarkerFieldFlags, MarkerFieldFormat,
+    MarkerTiming, ProcessHandle, Profile, StaticSchemaMarker, StaticSchemaMarkerField,
     StringHandle, ThreadHandle, Timestamp,
 };
 
@@ -157,23 +157,16 @@ pub struct ThreadNameMarkerForCpuTrack(pub StringHandle, pub StringHandle);
 impl StaticSchemaMarker for ThreadNameMarkerForCpuTrack {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "ContextSwitch";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.thread}".into()),
-            tooltip_label: Some("{marker.data.thread}".into()),
-            table_label: Some("{marker.name} - {marker.data.thread}".into()),
-            fields: vec![MarkerFieldSchema {
-                key: "thread".into(),
-                label: "Thread".into(),
-                format: MarkerFieldFormat::String,
-                searchable: true,
-            }],
-            static_fields: vec![],
-            graphs: vec![],
-        }
-    }
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.thread}");
+    const TOOLTIP_LABEL: Option<&'static str> = Some("{marker.data.thread}");
+    const TABLE_LABEL: Option<&'static str> = Some("{marker.name} - {marker.data.thread}");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "thread",
+        label: "Thread",
+        format: MarkerFieldFormat::String,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, _profile: &mut Profile) -> StringHandle {
         self.0
@@ -202,33 +195,25 @@ pub struct OnCpuMarkerForThreadTrack {
 impl StaticSchemaMarker for OnCpuMarkerForThreadTrack {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "OnCpu";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.cpu}".into()),
-            tooltip_label: Some("{marker.data.cpu}".into()),
-            table_label: Some(
-                "{marker.name} - {marker.data.cpu}, switch-out reason: {marker.data.outwhy}".into(),
-            ),
-            fields: vec![
-                MarkerFieldSchema {
-                    key: "cpu".into(),
-                    label: "CPU".into(),
-                    format: MarkerFieldFormat::String,
-                    searchable: true,
-                },
-                MarkerFieldSchema {
-                    key: "outwhy".into(),
-                    label: "Switch-out reason".into(),
-                    format: MarkerFieldFormat::String,
-                    searchable: true,
-                },
-            ],
-            static_fields: vec![],
-            graphs: vec![],
-        }
-    }
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.cpu}");
+    const TOOLTIP_LABEL: Option<&'static str> = Some("{marker.data.cpu}");
+    const TABLE_LABEL: Option<&'static str> =
+        Some("{marker.name} - {marker.data.cpu}, switch-out reason: {marker.data.outwhy}");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[
+        StaticSchemaMarkerField {
+            key: "cpu",
+            label: "CPU",
+            format: MarkerFieldFormat::String,
+            flags: MarkerFieldFlags::SEARCHABLE,
+        },
+        StaticSchemaMarkerField {
+            key: "outwhy",
+            label: "Switch-out reason",
+            format: MarkerFieldFormat::String,
+            flags: MarkerFieldFlags::SEARCHABLE,
+        },
+    ];
 
     fn name(&self, profile: &mut Profile) -> StringHandle {
         profile.intern_string("Running on CPU")
diff --git a/samply/src/shared/process_sample_data.rs b/samply/src/shared/process_sample_data.rs
index 45419628..61d24d76 100644
--- a/samply/src/shared/process_sample_data.rs
+++ b/samply/src/shared/process_sample_data.rs
@@ -1,7 +1,7 @@
 use fxprof_processed_profile::{
-    CategoryHandle, CategoryPairHandle, LibMappings, MarkerFieldFormat, MarkerFieldSchema,
-    MarkerLocation, MarkerSchema, MarkerStaticField, MarkerTiming, Profile, StaticSchemaMarker,
-    StringHandle, ThreadHandle, Timestamp,
+    CategoryHandle, CategoryPairHandle, LibMappings, MarkerFieldFlags, MarkerFieldFormat,
+    MarkerTiming, Profile, StaticSchemaMarker, StaticSchemaMarkerField, StringHandle, ThreadHandle,
+    Timestamp,
 };
 
 use super::lib_mappings::{LibMappingInfo, LibMappingOpQueue, LibMappingsHierarchy};
@@ -144,36 +144,28 @@ impl RssStatMarker {
 impl StaticSchemaMarker for RssStatMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "RSS Anon";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.totalBytes}".into()),
-            tooltip_label: Some("{marker.data.totalBytes}".into()),
-            table_label: Some(
-                "Total: {marker.data.totalBytes}, delta: {marker.data.deltaBytes}".into(),
-            ),
-            fields: vec![
-                MarkerFieldSchema {
-                    key: "totalBytes".into(),
-                    label: "Total bytes".into(),
-                    format: MarkerFieldFormat::Bytes,
-                    searchable: true,
-                },
-                MarkerFieldSchema {
-                    key: "deltaBytes".into(),
-                    label: "Delta".into(),
-                    format: MarkerFieldFormat::Bytes,
-                    searchable: true,
-                },
-            ],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value: "Emitted when the kmem:rss_stat tracepoint is hit.".into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.totalBytes}");
+    const TOOLTIP_LABEL: Option<&'static str> = Some("{marker.data.totalBytes}");
+    const TABLE_LABEL: Option<&'static str> =
+        Some("Total: {marker.data.totalBytes}, delta: {marker.data.deltaBytes}");
+
+    const DESCRIPTION: Option<&'static str> =
+        Some("Emitted when the kmem:rss_stat tracepoint is hit.");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[
+        StaticSchemaMarkerField {
+            key: "totalBytes",
+            label: "Total bytes",
+            format: MarkerFieldFormat::Bytes,
+            flags: MarkerFieldFlags::SEARCHABLE,
+        },
+        StaticSchemaMarkerField {
+            key: "deltaBytes",
+            label: "Delta",
+            format: MarkerFieldFormat::Bytes,
+            flags: MarkerFieldFlags::SEARCHABLE,
+        },
+    ];
 
     fn name(&self, _profile: &mut Profile) -> StringHandle {
         self.name
@@ -202,23 +194,10 @@ pub struct OtherEventMarker(pub StringHandle);
 impl StaticSchemaMarker for OtherEventMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "Other event";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: None,
-            tooltip_label: None,
-            table_label: None,
-            fields: vec![],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value:
-                    "Emitted for any records in a perf.data file which don't map to a known event."
-                        .into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const DESCRIPTION: Option<&'static str> =
+        Some("Emitted for any records in a perf.data file which don't map to a known event.");
+
+    const FIELDS: &'static [fxprof_processed_profile::StaticSchemaMarkerField] = &[];
 
     fn name(&self, _profile: &mut Profile) -> StringHandle {
         self.0
@@ -243,26 +222,19 @@ pub struct UserTimingMarker(pub StringHandle);
 impl StaticSchemaMarker for UserTimingMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "UserTiming";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.name}".into()),
-            tooltip_label: Some("{marker.data.name}".into()),
-            table_label: Some("{marker.data.name}".into()),
-            fields: vec![MarkerFieldSchema {
-                key: "name".into(),
-                label: "Name".into(),
-                format: MarkerFieldFormat::String,
-                searchable: true,
-            }],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value: "Emitted for performance.mark and performance.measure.".into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const DESCRIPTION: Option<&'static str> =
+        Some("Emitted for performance.mark and performance.measure.");
+
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.name}");
+    const TOOLTIP_LABEL: Option<&'static str> = Some("{marker.data.name}");
+    const TABLE_LABEL: Option<&'static str> = Some("{marker.data.name}");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "name",
+        label: "Name",
+        format: MarkerFieldFormat::String,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, profile: &mut Profile) -> StringHandle {
         profile.intern_string("UserTiming")
@@ -286,21 +258,10 @@ pub struct SchedSwitchMarkerOnCpuTrack;
 impl StaticSchemaMarker for SchedSwitchMarkerOnCpuTrack {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "sched_switch";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: None,
-            tooltip_label: None,
-            table_label: None,
-            fields: vec![],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value: "Emitted just before a running thread gets moved off-cpu.".into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const DESCRIPTION: Option<&'static str> =
+        Some("Emitted just before a running thread gets moved off-cpu.");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[];
 
     fn name(&self, profile: &mut Profile) -> StringHandle {
         profile.intern_string("sched_switch")
@@ -327,26 +288,15 @@ pub struct SchedSwitchMarkerOnThreadTrack {
 impl StaticSchemaMarker for SchedSwitchMarkerOnThreadTrack {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "sched_switch";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: None,
-            tooltip_label: None,
-            table_label: None,
-            fields: vec![MarkerFieldSchema {
-                key: "cpu".into(),
-                label: "cpu".into(),
-                format: MarkerFieldFormat::Integer,
-                searchable: true,
-            }],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value: "Emitted just before a running thread gets moved off-cpu.".into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const DESCRIPTION: Option<&'static str> =
+        Some("Emitted just before a running thread gets moved off-cpu.");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "cpu",
+        label: "cpu",
+        format: MarkerFieldFormat::Integer,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, profile: &mut Profile) -> StringHandle {
         profile.intern_string("sched_switch")
@@ -371,26 +321,19 @@ pub struct SimpleMarker(pub StringHandle);
 impl StaticSchemaMarker for SimpleMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "SimpleMarker";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.name}".into()),
-            tooltip_label: Some("{marker.data.name}".into()),
-            table_label: Some("{marker.data.name}".into()),
-            fields: vec![MarkerFieldSchema {
-                key: "name".into(),
-                label: "Name".into(),
-                format: MarkerFieldFormat::String,
-                searchable: true,
-            }],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value: "Emitted for marker spans in a markers text file.".into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const DESCRIPTION: Option<&'static str> =
+        Some("Emitted for marker spans in a markers text file.");
+
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.name}");
+    const TOOLTIP_LABEL: Option<&'static str> = Some("{marker.data.name}");
+    const TABLE_LABEL: Option<&'static str> = Some("{marker.data.name}");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "name",
+        label: "Name",
+        format: MarkerFieldFormat::String,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, profile: &mut Profile) -> StringHandle {
         profile.intern_string("SimpleMarker")
diff --git a/samply/src/windows/coreclr.rs b/samply/src/windows/coreclr.rs
index 7ae7a88c..2d9b341e 100644
--- a/samply/src/windows/coreclr.rs
+++ b/samply/src/windows/coreclr.rs
@@ -212,40 +212,32 @@ pub struct CoreClrGcAllocMarker(StringHandle, f64, CategoryHandle);
 impl StaticSchemaMarker for CoreClrGcAllocMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Alloc";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![
-                MarkerLocation::MarkerChart,
-                MarkerLocation::MarkerTable,
-                MarkerLocation::TimelineMemory,
-            ],
-            chart_label: Some("GC Alloc".into()),
-            tooltip_label: Some(
-                "GC Alloc: {marker.data.clrtype} ({marker.data.size} bytes)".into(),
-            ),
-            table_label: Some("GC Alloc".into()),
-            fields: vec![
-                MarkerFieldSchema {
-                    key: "clrtype".into(),
-                    label: "CLR Type".into(),
-                    format: MarkerFieldFormat::String,
-                    searchable: true,
-                },
-                MarkerFieldSchema {
-                    key: "size".into(),
-                    label: "Size".into(),
-                    format: MarkerFieldFormat::Bytes,
-                    searchable: false,
-                },
-            ],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value: "GC Allocation.".into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const DESCRIPTION: Option<&'static str> = Some("GC Allocation.");
+
+    const LOCATIONS: MarkerLocations = MarkerLocations::MARKER_CHART
+        .union(MarkerLocations::MARKER_TABLE)
+        .union(MarkerLocations::TIMELINE_MEMORY);
+
+    const CHART_LABEL: Option<&'static str> = Some("GC Alloc");
+    const TOOLTIP_LABEL: Option<&'static str> =
+        Some("GC Alloc: {marker.data.clrtype} ({marker.data.size} bytes)");
+    const TABLE_LABEL: Option<&'static str> =
+        Some("GC Alloc: {marker.data.clrtype} ({marker.data.size} bytes)");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[
+        StaticSchemaMarkerField {
+            key: "clrtype",
+            label: "CLR Type",
+            format: MarkerFieldFormat::String,
+            flags: MarkerFieldFlags::SEARCHABLE,
+        },
+        StaticSchemaMarkerField {
+            key: "size",
+            label: "Size",
+            format: MarkerFieldFormat::Bytes,
+            flags: MarkerFieldFlags::empty(),
+        },
+    ];
 
     fn name(&self, profile: &mut Profile) -> StringHandle {
         profile.intern_string("GC Alloc")
@@ -270,30 +262,22 @@ pub struct CoreClrGcEventMarker(StringHandle, StringHandle, CategoryHandle);
 impl StaticSchemaMarker for CoreClrGcEventMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "GC Event";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![
-                MarkerLocation::MarkerChart,
-                MarkerLocation::MarkerTable,
-                MarkerLocation::TimelineMemory,
-            ],
-            chart_label: Some("{marker.data.event}".into()),
-            tooltip_label: Some("{marker.data.event}".into()),
-            table_label: Some("{marker.data.event}".into()),
-            fields: vec![MarkerFieldSchema {
-                key: "event".into(),
-                label: "Event".into(),
-                format: MarkerFieldFormat::String,
-                searchable: true,
-            }],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value: "Generic GC Event.".into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const DESCRIPTION: Option<&'static str> = Some("Generic GC Event.");
+
+    const LOCATIONS: MarkerLocations = MarkerLocations::MARKER_CHART
+        .union(MarkerLocations::MARKER_TABLE)
+        .union(MarkerLocations::TIMELINE_MEMORY);
+
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.event}");
+    const TOOLTIP_LABEL: Option<&'static str> = Some("{marker.data.event}");
+    const TABLE_LABEL: Option<&'static str> = Some("{marker.name} - {marker.data.event}");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "event",
+        label: "Event",
+        format: MarkerFieldFormat::String,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, _profile: &mut Profile) -> StringHandle {
         self.0
@@ -758,26 +742,18 @@ pub struct OtherClrMarker(StringHandle, StringHandle);
 impl StaticSchemaMarker for OtherClrMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "OtherClrMarker";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.name}".into()),
-            tooltip_label: Some("{marker.data.name}".into()),
-            table_label: Some("{marker.data.name}".into()),
-            fields: vec![MarkerFieldSchema {
-                key: "name".into(),
-                label: "Name".into(),
-                format: MarkerFieldFormat::String,
-                searchable: true,
-            }],
-            static_fields: vec![MarkerStaticField {
-                label: "Description".into(),
-                value: "CoreCLR marker of unknown type.".into(),
-            }],
-            graphs: vec![],
-        }
-    }
+    const DESCRIPTION: Option<&'static str> = Some("CoreCLR marker of unknown type.");
+
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.name}");
+    const TOOLTIP_LABEL: Option<&'static str> = Some("{marker.data.name}");
+    const TABLE_LABEL: Option<&'static str> = Some("{marker.name} - {marker.data.name}");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "name",
+        label: "Name",
+        format: MarkerFieldFormat::String,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, _profile: &mut Profile) -> StringHandle {
         self.0
diff --git a/samply/src/windows/profile_context.rs b/samply/src/windows/profile_context.rs
index a20cd4fd..1dd668e7 100644
--- a/samply/src/windows/profile_context.rs
+++ b/samply/src/windows/profile_context.rs
@@ -4,9 +4,9 @@ use std::path::Path;
 use debugid::DebugId;
 use fxprof_processed_profile::{
     CategoryColor, CategoryHandle, CounterHandle, CpuDelta, Frame, FrameFlags, FrameInfo,
-    LibraryHandle, LibraryInfo, Marker, MarkerFieldFormat, MarkerFieldSchema, MarkerHandle,
-    MarkerLocation, MarkerSchema, MarkerTiming, ProcessHandle, Profile, SamplingInterval,
-    StaticSchemaMarker, StringHandle, ThreadHandle, Timestamp,
+    LibraryHandle, LibraryInfo, Marker, MarkerFieldFlags, MarkerFieldFormat, MarkerHandle,
+    MarkerLocations, MarkerTiming, ProcessHandle, Profile, SamplingInterval, StaticSchemaMarker,
+    StaticSchemaMarkerField, StringHandle, ThreadHandle, Timestamp,
 };
 use shlex::Shlex;
 use wholesym::PeCodeId;
@@ -1563,22 +1563,11 @@ impl ProfileContext {
         impl StaticSchemaMarker for VSyncMarker {
             const UNIQUE_MARKER_TYPE_NAME: &'static str = "Vsync";
 
-            fn schema() -> MarkerSchema {
-                MarkerSchema {
-                    type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-                    locations: vec![
-                        MarkerLocation::MarkerChart,
-                        MarkerLocation::MarkerTable,
-                        MarkerLocation::TimelineOverview,
-                    ],
-                    chart_label: Some("{marker.data.name}".into()),
-                    tooltip_label: None,
-                    table_label: Some("{marker.name}".into()),
-                    fields: vec![],
-                    static_fields: vec![],
-                    graphs: vec![],
-                }
-            }
+            const LOCATIONS: MarkerLocations = MarkerLocations::MARKER_CHART
+                .union(MarkerLocations::MARKER_TABLE)
+                .union(MarkerLocations::TIMELINE_OVERVIEW);
+
+            const FIELDS: &'static [StaticSchemaMarkerField] = &[];
 
             fn name(&self, profile: &mut Profile) -> StringHandle {
                 profile.intern_string("Vsync")
@@ -2243,23 +2232,16 @@ pub struct FreeformMarker(StringHandle, StringHandle, CategoryHandle);
 impl StaticSchemaMarker for FreeformMarker {
     const UNIQUE_MARKER_TYPE_NAME: &'static str = "FreeformMarker";
 
-    fn schema() -> MarkerSchema {
-        MarkerSchema {
-            type_name: Self::UNIQUE_MARKER_TYPE_NAME.into(),
-            locations: vec![MarkerLocation::MarkerChart, MarkerLocation::MarkerTable],
-            chart_label: Some("{marker.data.values}".into()),
-            tooltip_label: Some("{marker.name} - {marker.data.values}".into()),
-            table_label: Some("{marker.data.values}".into()),
-            fields: vec![MarkerFieldSchema {
-                key: "values".into(),
-                label: "Values".into(),
-                format: MarkerFieldFormat::String,
-                searchable: true,
-            }],
-            static_fields: vec![],
-            graphs: vec![],
-        }
-    }
+    const CHART_LABEL: Option<&'static str> = Some("{marker.data.values}");
+    const TOOLTIP_LABEL: Option<&'static str> = Some("{marker.name} - {marker.data.values}");
+    const TABLE_LABEL: Option<&'static str> = Some("{marker.name} - {marker.data.values");
+
+    const FIELDS: &'static [StaticSchemaMarkerField] = &[StaticSchemaMarkerField {
+        key: "values",
+        label: "Values",
+        format: MarkerFieldFormat::String,
+        flags: MarkerFieldFlags::SEARCHABLE,
+    }];
 
     fn name(&self, _profile: &mut Profile) -> StringHandle {
         self.0