diff --git a/utilities/perfetto-custom-events/README.md b/utilities/perfetto-custom-events/README.md
new file mode 100644
index 0000000..82cd09d
--- /dev/null
+++ b/utilities/perfetto-custom-events/README.md
@@ -0,0 +1,81 @@
+# Perfetto Custom Events
+
+This sample shows how to generate various custom Perfetto events from an app.
+
+## Instantaneous Events
+
+Instantaneous Events are the simplest. They have no duration, but can have additonal paramaters associated with them. In `main.brs` the app generates two of these events, one with parameters and one without.
+
+```
+ ' instantaneous events
+ tracer.instantEvent("instantaneous_event")
+ ' instantaneous event with parameters
+ tracer.instantEvent("instantaneous_event_params", {p1: 42, p2: "hello world"})
+```
+
+## Duration Events
+
+Duration events have a beginning and an end. In addition, you can nest them so that they appear as a stack in the Perfetto viewer. In `main.brs` the app generates a series of three nested duration events.
+
+```
+ ' duration events
+ tracer.beginEvent("duration_event")
+ sleep(500)
+ ' events can be nested and will be shwon as a stack in the Perfetto viewer
+ tracer.beginEvent("duration_subevent_1")
+ sleep(500)
+ tracer.beginEvent("duration_subevent_2")
+ sleep(500)
+ ' it is important that the number of endEvent() calls matches the number of beginEvent() calls
+ tracer.endEvent()
+ sleep(500)
+ tracer.endEvent()
+ tracer.endEvent()
+```
+
+# Scoped Events
+
+Scoped events are similar to duration events, except that you do not end them explicitly. They end automatically when the returned object goes out of scope. In `main.brs` the app defines a function `DoScopedEvents()` that demonstrates this mechanism.
+
+```
+sub DoScopedEvent()
+ tracer = CreateObject("roPerfetto")
+ scoped_event = tracer.createScopedEvent("scoped_event")
+ sleep(500)
+ ' the event will end automatically when scoped_event goes out of scope
+end sub
+```
+
+## Flow Events
+
+Flow events allow you to connect a series of events. In the Perfetto viewer they will be shown with arrows between them. In `MyTask.brs` the app generates a series of flow events starting in its `Init()` and continuing in `MyTaskFunction()`. Then, in `MyScene.brs` the app terminates the flow in the observer function `OnTaskDone()`
+
+```
+sub Init()
+ tracer = CreateObject("roPerfetto")
+ ' flow id can be any integer value
+ ' it is used to correlate flow events, so it's important to use the same id
+ ' for all events that are part of the same flow
+ flow_id = 42
+ tracer.flowEvent(flow_id, "task_init")
+
+ m.top.functionName = "MyTaskFunction"
+end sub
+
+sub MyTaskFunction()
+ tracer = CreateObject("roPerfetto")
+ flow_id = 42 ' flow id must match the one used to start the flow
+ tracer.flowEvent(flow_id, "task_function")
+
+ sleep(50) ' simulate work being done
+
+ m.top.done = true
+end sub
+```
+```
+sub OnTaskDone()
+ tracer = CreateObject("roPerfetto")
+ flow_id = 42 ' flow id must match the one used to start the flow
+ tracer.terminateFlow(flow_id, "task_done")
+end sub
+```
\ No newline at end of file
diff --git a/utilities/perfetto-custom-events/components/MyScene.brs b/utilities/perfetto-custom-events/components/MyScene.brs
new file mode 100644
index 0000000..ebec572
--- /dev/null
+++ b/utilities/perfetto-custom-events/components/MyScene.brs
@@ -0,0 +1,13 @@
+' Copyright (c) 2025 Roku, Inc. All rights reserved.
+
+sub Init()
+ my_task = m.top.FindNode("myTask")
+ my_task.ObserveField("done", "OnTaskDone")
+ my_task.control = "run"
+end sub
+
+sub OnTaskDone()
+ tracer = CreateObject("roPerfetto")
+ flow_id = 42 ' flow id must match the one used to start the flow
+ tracer.terminateFlow(flow_id, "task_done")
+end sub
diff --git a/utilities/perfetto-custom-events/components/MyScene.xml b/utilities/perfetto-custom-events/components/MyScene.xml
new file mode 100755
index 0000000..f444522
--- /dev/null
+++ b/utilities/perfetto-custom-events/components/MyScene.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/utilities/perfetto-custom-events/components/MyTask.brs b/utilities/perfetto-custom-events/components/MyTask.brs
new file mode 100644
index 0000000..73a0608
--- /dev/null
+++ b/utilities/perfetto-custom-events/components/MyTask.brs
@@ -0,0 +1,22 @@
+' Copyright (c) 2025 Roku, Inc. All rights reserved.
+
+sub Init()
+ tracer = CreateObject("roPerfetto")
+ ' flow id can be any integer value
+ ' it is used to correlate flow events, so it's important to use the same id
+ ' for all events that are part of the same flow
+ flow_id = 42
+ tracer.flowEvent(flow_id, "task_init")
+
+ m.top.functionName = "MyTaskFunction"
+end sub
+
+sub MyTaskFunction()
+ tracer = CreateObject("roPerfetto")
+ flow_id = 42 ' flow id must match the one used to start the flow
+ tracer.flowEvent(flow_id, "task_function")
+
+ sleep(50) ' simulate work being done
+
+ m.top.done = true
+end sub
diff --git a/utilities/perfetto-custom-events/components/MyTask.xml b/utilities/perfetto-custom-events/components/MyTask.xml
new file mode 100755
index 0000000..d289320
--- /dev/null
+++ b/utilities/perfetto-custom-events/components/MyTask.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/utilities/perfetto-custom-events/images/rde_mm_focus_hd.jpg b/utilities/perfetto-custom-events/images/rde_mm_focus_hd.jpg
new file mode 100755
index 0000000..c2de09c
Binary files /dev/null and b/utilities/perfetto-custom-events/images/rde_mm_focus_hd.jpg differ
diff --git a/utilities/perfetto-custom-events/images/rde_mm_focus_sd.jpg b/utilities/perfetto-custom-events/images/rde_mm_focus_sd.jpg
new file mode 100755
index 0000000..ae50549
Binary files /dev/null and b/utilities/perfetto-custom-events/images/rde_mm_focus_sd.jpg differ
diff --git a/utilities/perfetto-custom-events/images/rde_splash_fhd.jpg b/utilities/perfetto-custom-events/images/rde_splash_fhd.jpg
new file mode 100755
index 0000000..379781b
Binary files /dev/null and b/utilities/perfetto-custom-events/images/rde_splash_fhd.jpg differ
diff --git a/utilities/perfetto-custom-events/images/rde_splash_hd.jpg b/utilities/perfetto-custom-events/images/rde_splash_hd.jpg
new file mode 100755
index 0000000..5db88f0
Binary files /dev/null and b/utilities/perfetto-custom-events/images/rde_splash_hd.jpg differ
diff --git a/utilities/perfetto-custom-events/images/rde_splash_sd.jpg b/utilities/perfetto-custom-events/images/rde_splash_sd.jpg
new file mode 100755
index 0000000..60055b3
Binary files /dev/null and b/utilities/perfetto-custom-events/images/rde_splash_sd.jpg differ
diff --git a/utilities/perfetto-custom-events/manifest b/utilities/perfetto-custom-events/manifest
new file mode 100755
index 0000000..be52a01
--- /dev/null
+++ b/utilities/perfetto-custom-events/manifest
@@ -0,0 +1,19 @@
+title=Sample - roPerfetto Custom Events
+major_version=1
+minor_version=0
+build_version=1
+
+mm_icon_focus_hd=pkg:/images/rde_mm_focus_hd.jpg
+mm_icon_focus_sd=pkg:/images/rde_mm_focus_sd.jpg
+
+splash_screen_sd=pkg:/images/rde_splash_sd.jpg
+splash_screen_hd=pkg:/images/rsgde_splash_hd.jpg
+splash_screen_fhd=pkg:/images/rde_splash_fhd.jpg
+
+splash_color=#662D91
+splash_min_time=1000
+
+ui_resolutions=hd
+rsg_version=1.2
+
+run_as_process=1
diff --git a/utilities/perfetto-custom-events/source/main.brs b/utilities/perfetto-custom-events/source/main.brs
new file mode 100755
index 0000000..0e8836d
--- /dev/null
+++ b/utilities/perfetto-custom-events/source/main.brs
@@ -0,0 +1,50 @@
+' Copyright (c) 2025 Roku, Inc. All rights reserved.
+
+sub Main(args)
+ ' you can reuse a single roPerfetto object to record multiple events
+ tracer = CreateObject("roPerfetto")
+
+ ' instantaneous events
+ tracer.instantEvent("instantaneous_event")
+ ' instantaneous event with parameters
+ tracer.instantEvent("instantaneous_event_params", {p1: 42, p2: "hello world"})
+
+ ' duration events
+ tracer.beginEvent("duration_event")
+ sleep(500)
+ ' events can be nested and will be shwon as a stack in the Perfetto viewer
+ tracer.beginEvent("duration_subevent_1")
+ sleep(500)
+ tracer.beginEvent("duration_subevent_2")
+ sleep(500)
+ ' it is important that the number of endEvent() calls matches the number of beginEvent() calls
+ tracer.endEvent()
+ sleep(500)
+ tracer.endEvent()
+ tracer.endEvent()
+
+ ' scoped events
+ DoScopedEvent()
+
+ screen = CreateObject("roSGScreen")
+ port = CreateObject("roMessagePort")
+ screen.setMessagePort(port)
+ scene = screen.CreateScene("MyScene")
+ screen.show()
+
+ while(true)
+ msg = wait(0, port)
+ msgType = type(msg)
+
+ if msgType = "roSGScreenEvent"
+ if msg.isScreenClosed() then return
+ end if
+ end while
+end sub
+
+sub DoScopedEvent()
+ tracer = CreateObject("roPerfetto")
+ scoped_event = tracer.createScopedEvent("scoped_event")
+ sleep(500)
+ ' the event will end automatically when scoped_event goes out of scope
+end sub