Add support for `--trace-to` for python and use it in repl tests (#28179)

* Start adding tracing start/stop functions

* Add raii-like support for tracing

* Add raii-like support for tracing

* Fix compile logic

* Switch linux, mac, android to C++17 by default

* Start outputting trace data

* Upload traces that are gathered

* Fix names

* Allow placeholders in script args too

* Allow from-string tracing

* Do not newline-separate restyle path otherwise only the first argument is processed

* Restyle

* Add some additional types

* Minor python fixes

* Import ctypes

* Things run now

* Add trace bits to our tests

* Undo restyle-diff change

* Fix some typos in naming

* Add perfetto for darwin too

* mobile-device-test.py does not suppor trace-to yet

* Make mobile device test also be able to trace. Mobile device test seems super slow

* Restyled by autopep8

* Restyled by isort

---------

Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 99f7055..c462203 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -437,24 +437,25 @@
                    "
             - name: Run Tests
               run: |
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --script-args "--log-level INFO -t 3600 --disable-test ClusterObjectTests.TestTimedRequestTimeout"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_RR_1_1.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_DeviceBasicComposition.py" --script-args "--storage-path admin_storage.json --manual-code 10054912339"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_SC_3_6.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_DA_1_7.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --bool-arg allow_sdk_dac:true"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log --enable-key 000102030405060708090a0b0c0d0e0f" --script "src/python_testing/TC_TestEventTrigger.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --bool-arg allow_sdk_dac:true"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_ACE_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_ACE_1_4.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --int-arg PIXIT.ACE.APPENDPOINT:1 PIXIT.ACE.APPDEVTYPEID:0x0100 --string-arg PIXIT.ACE.APPCLUSTER:OnOff PIXIT.ACE.APPATTRIBUTE:OnOff"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_ACE_1_3.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_CGEN_2_4.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_DA_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_TIMESYNC_3_1.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_DA_1_5.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_IDM_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_FAN_3_3.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_FAN_3_4.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:log" --script "src/python_testing/TC_FAN_3_5.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021"'
-                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestMatterTestingSupport.py"'
+                  mkdir -p out/trace_data
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app --factoryreset --app-args "--trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script-args "--log-level INFO -t 3600 --disable-test ClusterObjectTests.TestTimedRequestTimeout --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_RR_1_1.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_DeviceBasicComposition.py" --script-args "--storage-path admin_storage.json --manual-code 10054912339 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_SC_3_6.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_DA_1_7.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --bool-arg allow_sdk_dac:true --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json --enable-key 000102030405060708090a0b0c0d0e0f" --script "src/python_testing/TC_TestEventTrigger.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --bool-arg allow_sdk_dac:true --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_ACE_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_ACE_1_4.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --int-arg PIXIT.ACE.APPENDPOINT:1 PIXIT.ACE.APPDEVTYPEID:0x0100 --string-arg PIXIT.ACE.APPCLUSTER:OnOff PIXIT.ACE.APPATTRIBUTE:OnOff --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_ACE_1_3.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_CGEN_2_4.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_DA_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_TIMESYNC_3_1.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_DA_1_5.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_IDM_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_FAN_3_3.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_FAN_3_4.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_FAN_3_5.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TestMatterTestingSupport.py" --script-args "--trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
             - name: Uploading core files
               uses: actions/upload-artifact@v3
               if: ${{ failure() && !env.ACT }}
@@ -524,6 +525,13 @@
                   path: /cores/
                   # Cores are big; don't hold on to them too long.
                   retention-days: 5
+            - name: Uploading traces on failure
+              uses: actions/upload-artifact@v3
+              if: ${{ failure() && !env.ACT }}
+              with:
+                  name: trace-data-python-repl
+                  path: out/trace_data/
+                  retention-days: 5
             - name: Uploading diagnostic logs
               uses: actions/upload-artifact@v3
               if: ${{ failure() && !env.ACT }}
diff --git a/.gitmodules b/.gitmodules
index 990cb20..1508216 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -307,7 +307,7 @@
 	path = third_party/perfetto/repo
 	url = https://github.com/google/perfetto.git
 	branch = master
-	platforms = linux,android
+	platforms = linux,android,darwin
 [submodule "third_party/asr/components"]
 	path = third_party/asr/components
 	url = https://github.com/asriot/asriot_components.git
diff --git a/scripts/tests/run_python_test.py b/scripts/tests/run_python_test.py
index c607a5d..33931ac 100755
--- a/scripts/tests/run_python_test.py
+++ b/scripts/tests/run_python_test.py
@@ -17,6 +17,7 @@
 import datetime
 import logging
 import os
+import os.path
 import queue
 import re
 import shlex
@@ -72,7 +73,7 @@
 @click.option("--factoryreset", is_flag=True,
               help='Remove app config and repl configs (/tmp/chip* and /tmp/repl*) before running the tests.')
 @click.option("--app-args", type=str, default='',
-              help='The extra arguments passed to the device.')
+              help='The extra arguments passed to the device. Can use placholders like {SCRIPT_BASE_NAME}')
 @click.option("--script", type=click.Path(exists=True), default=os.path.join(DEFAULT_CHIP_ROOT,
                                                                              'src',
                                                                              'controller',
@@ -81,10 +82,13 @@
                                                                              'test_scripts',
                                                                              'mobile-device-test.py'), help='Test script to use.')
 @click.option("--script-args", type=str, default='',
-              help='Path to the test script to use, omit to use the default test script (mobile-device-test.py).')
+              help='Script arguments, can use placeholders like {SCRIPT_BASE_NAME}.')
 @click.option("--script-gdb", is_flag=True,
               help='Run script through gdb')
 def main(app: str, factoryreset: bool, app_args: str, script: str, script_args: str, script_gdb: bool):
+    app_args = app_args.replace('{SCRIPT_BASE_NAME}', os.path.splitext(os.path.basename(script))[0])
+    script_args = script_args.replace('{SCRIPT_BASE_NAME}', os.path.splitext(os.path.basename(script))[0])
+
     if factoryreset:
         # Remove native app config
         retcode = subprocess.call("rm -rf /tmp/chip* /tmp/repl*", shell=True)
diff --git a/src/controller/python/BUILD.gn b/src/controller/python/BUILD.gn
index c76c64c..375a65c 100644
--- a/src/controller/python/BUILD.gn
+++ b/src/controller/python/BUILD.gn
@@ -78,6 +78,7 @@
       "chip/internal/CommissionerImpl.cpp",
       "chip/logging/LoggingRedirect.cpp",
       "chip/native/PyChipError.cpp",
+      "chip/tracing/TracingSetup.cpp",
       "chip/utils/DeviceProxyUtils.cpp",
     ]
     defines += [ "CHIP_CONFIG_MAX_GROUPS_PER_FABRIC=50" ]
@@ -120,8 +121,16 @@
     public_deps += [
       "${chip_root}/src/controller/data_model",
       "${chip_root}/src/credentials:file_attestation_trust_store",
+      "${chip_root}/src/tracing/json",
+      "${chip_root}/src/tracing/perfetto",
+      "${chip_root}/src/tracing/perfetto:file_output",
       "${chip_root}/third_party/jsoncpp",
     ]
+
+    deps = [
+      "${chip_root}/src/tracing/perfetto:event_storage",
+      "${chip_root}/src/tracing/perfetto:simple_initialization",
+    ]
   } else {
     public_deps += [ "$chip_data_model" ]
   }
@@ -238,6 +247,7 @@
         "chip/setup_payload/__init__.py",
         "chip/setup_payload/setup_payload.py",
         "chip/storage/__init__.py",
+        "chip/tracing/__init__.py",
         "chip/utils/CommissioningBuildingBlocks.py",
         "chip/utils/__init__.py",
         "chip/yaml/__init__.py",
@@ -292,6 +302,7 @@
     "chip.clusters",
     "chip.setup_payload",
     "chip.storage",
+    "chip.tracing",
   ]
 
   if (!chip_controller) {
diff --git a/src/controller/python/chip/native/__init__.py b/src/controller/python/chip/native/__init__.py
index 26e394e..7801129 100644
--- a/src/controller/python/chip/native/__init__.py
+++ b/src/controller/python/chip/native/__init__.py
@@ -197,5 +197,9 @@
     _GetLibraryHandle(False).pychip_CommonStackInit(ctypes.c_char_p(params))
 
 
-def GetLibraryHandle():
-    return _GetLibraryHandle(True)
+class HandleFlags(enum.Flag):
+    REQUIRE_INITIALIZATION = enum.auto()
+
+
+def GetLibraryHandle(flags=HandleFlags.REQUIRE_INITIALIZATION):
+    return _GetLibraryHandle(HandleFlags.REQUIRE_INITIALIZATION in flags)
diff --git a/src/controller/python/chip/tracing/TracingSetup.cpp b/src/controller/python/chip/tracing/TracingSetup.cpp
new file mode 100644
index 0000000..d09a655
--- /dev/null
+++ b/src/controller/python/chip/tracing/TracingSetup.cpp
@@ -0,0 +1,104 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include <controller/python/chip/native/PyChipError.h>
+#include <platform/PlatformManager.h>
+
+#include <tracing/json/json_tracing.h>
+#include <tracing/perfetto/event_storage.h>
+#include <tracing/perfetto/file_output.h>
+#include <tracing/perfetto/perfetto_tracing.h>
+#include <tracing/perfetto/simple_initialize.h>
+#include <tracing/registry.h>
+
+namespace {
+
+using chip::DeviceLayer::PlatformMgr;
+
+class ScopedStackLock
+{
+public:
+    ScopedStackLock() { PlatformMgr().LockChipStack(); }
+
+    ~ScopedStackLock() { PlatformMgr().UnlockChipStack(); }
+};
+
+chip::Tracing::Json::JsonBackend gJsonBackend;
+
+chip::Tracing::Perfetto::FileTraceOutput gPerfettoFileOutput;
+chip::Tracing::Perfetto::PerfettoBackend gPerfettoBackend;
+
+} // namespace
+
+extern "C" void pychip_tracing_start_json_log(const char * file_name)
+{
+
+    ScopedStackLock lock;
+
+    gJsonBackend.CloseFile(); // just in case, ensure no file output
+    chip::Tracing::Register(gJsonBackend);
+}
+
+extern "C" PyChipError pychip_tracing_start_json_file(const char * file_name)
+{
+    ScopedStackLock lock;
+
+    CHIP_ERROR err = gJsonBackend.OpenFile(file_name);
+    if (err != CHIP_NO_ERROR)
+    {
+        return ToPyChipError(err);
+    }
+    chip::Tracing::Register(gJsonBackend);
+    return ToPyChipError(CHIP_NO_ERROR);
+}
+
+extern "C" void pychip_tracing_start_perfetto_system()
+{
+    ScopedStackLock lock;
+
+    chip::Tracing::Perfetto::Initialize(perfetto::kSystemBackend);
+    chip::Tracing::Perfetto::RegisterEventTrackingStorage();
+    chip::Tracing::Register(gPerfettoBackend);
+}
+
+extern "C" PyChipError pychip_tracing_start_perfetto_file(const char * file_name)
+{
+    ScopedStackLock lock;
+
+    chip::Tracing::Perfetto::Initialize(perfetto::kInProcessBackend);
+    chip::Tracing::Perfetto::RegisterEventTrackingStorage();
+
+    CHIP_ERROR err = gPerfettoFileOutput.Open(file_name);
+    if (err != CHIP_NO_ERROR)
+    {
+        return ToPyChipError(err);
+    }
+    chip::Tracing::Register(gPerfettoBackend);
+
+    return ToPyChipError(CHIP_NO_ERROR);
+}
+
+extern "C" void pychip_tracing_stop()
+{
+    ScopedStackLock lock;
+
+    chip::Tracing::Perfetto::FlushEventTrackingStorage();
+    gPerfettoFileOutput.Close();
+    chip::Tracing::Unregister(gPerfettoBackend);
+    chip::Tracing::Unregister(gJsonBackend);
+}
diff --git a/src/controller/python/chip/tracing/__init__.py b/src/controller/python/chip/tracing/__init__.py
new file mode 100644
index 0000000..fc6ee35
--- /dev/null
+++ b/src/controller/python/chip/tracing/__init__.py
@@ -0,0 +1,125 @@
+#
+#    Copyright (c) 2023 Project CHIP Authors
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+import ctypes
+from enum import Enum, auto
+from typing import Optional
+
+import chip.native
+from chip.native import PyChipError
+
+
+def _GetTracingLibraryHandle() -> ctypes.CDLL:
+    """ Get the native library handle with tracing methods initialized.
+
+      Retreives the CHIP native library handle and attaches signatures to
+      native methods.
+      """
+
+    # Getting a handle without requiring init, as tracing methods
+    # do not require chip stack startup
+    handle = chip.native.GetLibraryHandle(chip.native.HandleFlags(0))
+
+    # Uses one of the type decorators as an indicator for everything being
+    # initialized.
+    if not handle.pychip_tracing_start_json_file.argtypes:
+        setter = chip.native.NativeLibraryHandleMethodArguments(handle)
+
+        setter.Set('pychip_tracing_start_json_log', None, [])
+        setter.Set('pychip_tracing_start_json_file', PyChipError, [ctypes.c_char_p])
+
+        setter.Set('pychip_tracing_start_perfetto_system', None, [])
+        setter.Set('pychip_tracing_start_perfetto_file', PyChipError, [ctypes.c_char_p])
+
+        setter.Set('pychip_tracing_stop', None, [])
+
+    return handle
+
+
+class TraceType(Enum):
+    JSON = auto()
+    PERFETTO = auto()
+
+
+def StartTracingTo(trace_type: TraceType, file_name: Optional[str] = None):
+    """
+    Initiate tracing to the specified destination.
+
+    Note that only one active trace can exist of a given type (i.e. cannot trace both
+    to files and logs/system).
+    """
+    handle = _GetTracingLibraryHandle()
+
+    if trace_type == TraceType.JSON:
+        if file_name is None:
+            handle.pychip_tracing_start_json_log()
+        else:
+            handle.pychip_tracing_start_json_file(file_name.encode('utf-8')).raise_on_error()
+    elif trace_type == TraceType.PERFETTO:
+        if file_name is None:
+            handle.pychip_tracing_start_perfetto_system()
+        else:
+            handle.pychip_tracing_start_perfetto_file(file_name.encode('utf-8')).raise_on_error()
+    else:
+        raise ValueError("unknown trace type")
+
+
+def StopTracing():
+    """
+    Make sure tracing is stopped.
+
+    MUST be called before application exits.
+    """
+    _GetTracingLibraryHandle().pychip_tracing_stop()
+
+
+class TracingContext:
+    """Allows scoped enter/exit for tracing, like:
+
+    with TracingContext() as tracing:
+       tracing.Start(TraceType.JSON)
+       # ...
+
+    """
+
+    def Start(self, trace_type: TraceType, file_name: Optional[str] = None):
+        StartTracingTo(trace_type, file_name)
+
+    def StartFromString(self, destination: str):
+        """
+        Convert a human string to a perfetto start.
+
+        Supports json:log, json:path, perfetto, perfetto:path
+        """
+        if destination == 'perfetto':
+            self.Start(TraceType.PERFETTO)
+        elif destination == 'json:log':
+            self.Start(TraceType.JSON)
+        elif destination.startswith("json:"):
+            self.Start(TraceType.JSON, destination[5:])
+        elif destination.startswith("perfetto:"):
+            self.Start(TraceType.PERFETTO, destination[9:])
+        else:
+            raise ValueError("Invalid trace-to destination: %r", destination)
+
+    def __init__(self):
+        pass
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        StopTracing()
diff --git a/src/controller/python/test/test_scripts/mobile-device-test.py b/src/controller/python/test/test_scripts/mobile-device-test.py
index 09f13cc..87e0a9a 100755
--- a/src/controller/python/test/test_scripts/mobile-device-test.py
+++ b/src/controller/python/test/test_scripts/mobile-device-test.py
@@ -27,6 +27,7 @@
 import click
 import coloredlogs
 from base import BaseTestHelper, FailIfNot, SetTestSet, TestFail, TestTimeout, logger
+from chip.tracing import TracingContext
 from cluster_objects import ClusterObjectTests
 from network_commissioning import NetworkCommissioningTests
 
@@ -246,8 +247,12 @@
               default='',
               type=str,
               help="Path that contains valid and trusted PAA Root Certificates.")
+@click.option('--trace-to',
+              multiple=True,
+              default=[],
+              help="Trace location")
 def run(controller_nodeid, device_nodeid, address, timeout, discriminator, setup_pin, enable_test, disable_test, log_level,
-        log_format, print_test_list, paa_trust_store_path):
+        log_format, print_test_list, paa_trust_store_path, trace_to):
     coloredlogs.install(level=log_level, fmt=log_format, logger=logger)
 
     if print_test_list:
@@ -267,8 +272,12 @@
     logger.info(f"\tEnabled Tests:     {enable_test}")
     logger.info(f"\tDisabled Tests:    {disable_test}")
     SetTestSet(enable_test, disable_test)
-    do_tests(controller_nodeid, device_nodeid, address, timeout,
-             discriminator, setup_pin, paa_trust_store_path)
+    with TracingContext() as tracing_ctx:
+        for destination in trace_to:
+            tracing_ctx.StartFromString(destination)
+
+        do_tests(controller_nodeid, device_nodeid, address, timeout,
+                 discriminator, setup_pin, paa_trust_store_path)
 
 
 if __name__ == "__main__":
diff --git a/src/python_testing/matter_testing_support.py b/src/python_testing/matter_testing_support.py
index b51da99..7570f3d 100644
--- a/src/python_testing/matter_testing_support.py
+++ b/src/python_testing/matter_testing_support.py
@@ -51,6 +51,7 @@
 from chip.clusters.Attribute import EventReadResult, SubscriptionTransaction
 from chip.interaction_model import InteractionModelError, Status
 from chip.storage import PersistentStorage
+from chip.tracing import TracingContext
 from mobly import asserts, base_test, signals, utils
 from mobly.config_parser import ENV_MOBLY_LOGPATH, TestRunConfig
 from mobly.test_runner import TestRunner
@@ -265,6 +266,8 @@
     # If this is set, we will reuse root of trust keys at that location
     chip_tool_credentials_path: Optional[pathlib.Path] = None
 
+    trace_to: List[str] = field(default_factory=list)
+
 
 class ClusterMapper:
     """Describe clusters/attributes using schema names."""
@@ -817,6 +820,7 @@
     config.pics = {} if args.PICS is None else read_pics_from_file(args.PICS)
 
     config.controller_node_id = args.controller_node_id
+    config.trace_to = args.trace_to
 
     # Accumulate all command-line-passed named args
     all_global_args = []
@@ -847,7 +851,8 @@
                              type=str,
                              metavar='test_a test_b...',
                              help='A list of tests in the test class to execute.')
-
+    basic_group.add_argument('--trace-to', nargs="*", default=[],
+                             help="Where to trace (e.g perfetto, perfetto:path, json:log, json:path)")
     basic_group.add_argument('--storage-path', action="store", type=pathlib.Path,
                              metavar="PATH", help="Location for persisted storage of instance")
     basic_group.add_argument('--logs-path', action="store", type=pathlib.Path, metavar="PATH", help="Location for test logs")
@@ -1051,44 +1056,49 @@
         matter_test_config.maximize_cert_chains = kwargs["maximize_cert_chains"]
 
     stack = MatterStackState(matter_test_config)
-    test_config.user_params["matter_stack"] = stash_globally(stack)
 
-    # TODO: Steer to right FabricAdmin!
-    # TODO: If CASE Admin Subject is a CAT tag range, then make sure to issue NOC with that CAT tag
+    with TracingContext() as tracing_ctx:
+        for destination in matter_test_config.trace_to:
+            tracing_ctx.StartFromString(destination)
 
-    default_controller = stack.certificate_authorities[0].adminList[0].NewController(
-        nodeId=matter_test_config.controller_node_id,
-        paaTrustStorePath=str(matter_test_config.paa_trust_store_path),
-        catTags=matter_test_config.controller_cat_tags
-    )
-    test_config.user_params["default_controller"] = stash_globally(default_controller)
+        test_config.user_params["matter_stack"] = stash_globally(stack)
 
-    test_config.user_params["matter_test_config"] = stash_globally(matter_test_config)
+        # TODO: Steer to right FabricAdmin!
+        # TODO: If CASE Admin Subject is a CAT tag range, then make sure to issue NOC with that CAT tag
 
-    test_config.user_params["certificate_authority_manager"] = stash_globally(stack.certificate_authority_manager)
+        default_controller = stack.certificate_authorities[0].adminList[0].NewController(
+            nodeId=matter_test_config.controller_node_id,
+            paaTrustStorePath=str(matter_test_config.paa_trust_store_path),
+            catTags=matter_test_config.controller_cat_tags
+        )
+        test_config.user_params["default_controller"] = stash_globally(default_controller)
 
-    # Execute the test class with the config
-    ok = True
+        test_config.user_params["matter_test_config"] = stash_globally(matter_test_config)
 
-    runner = TestRunner(log_dir=test_config.log_path,
-                        testbed_name=test_config.testbed_name)
+        test_config.user_params["certificate_authority_manager"] = stash_globally(stack.certificate_authority_manager)
 
-    with runner.mobly_logger():
-        if matter_test_config.commissioning_method is not None:
-            runner.add_test_class(test_config, CommissionDeviceTest, None)
+        # Execute the test class with the config
+        ok = True
 
-        # Add the tests selected unless we have a commission-only request
-        if not matter_test_config.commission_only:
-            runner.add_test_class(test_config, test_class, tests)
+        runner = TestRunner(log_dir=test_config.log_path,
+                            testbed_name=test_config.testbed_name)
 
-        try:
-            runner.run()
-            ok = runner.results.is_all_pass and ok
-        except signals.TestAbortAll:
-            ok = False
-        except Exception:
-            logging.exception('Exception when executing %s.', test_config.testbed_name)
-            ok = False
+        with runner.mobly_logger():
+            if matter_test_config.commissioning_method is not None:
+                runner.add_test_class(test_config, CommissionDeviceTest, None)
+
+            # Add the tests selected unless we have a commission-only request
+            if not matter_test_config.commission_only:
+                runner.add_test_class(test_config, test_class, tests)
+
+            try:
+                runner.run()
+                ok = runner.results.is_all_pass and ok
+            except signals.TestAbortAll:
+                ok = False
+            except Exception:
+                logging.exception('Exception when executing %s.', test_config.testbed_name)
+                ok = False
 
     # Shutdown the stack when all done
     stack.Shutdown()