diff --git a/.github/actions/build/action.yaml b/.github/actions/build/action.yaml
index 28616c4afed4..927ec0bec326 100644
--- a/.github/actions/build/action.yaml
+++ b/.github/actions/build/action.yaml
@@ -17,14 +17,14 @@ runs:
run: |
set -x
env
- if [ -z ${COBALT_BOOTLOADER+x} ]; then
+ if [ -z ${COBALT_EVERGREEN_LOADER+x} ]; then
BUILD_PLATFORM=${{ matrix.target_platform }}
BUILD_TARGET=all
if [[ "${{matrix.config}}" =~ ^(qa|gold)$ ]]; then
BUILD_TARGET=default
fi
else
- BUILD_PLATFORM=${COBALT_BOOTLOADER}
+ BUILD_PLATFORM=${COBALT_EVERGREEN_LOADER}
BUILD_TARGET='loader_app_install elf_loader_sandbox_install native_target/crashpad_handler'
fi
# GitHub Runners have home set to /github/home.
diff --git a/.github/actions/gn/action.yaml b/.github/actions/gn/action.yaml
index 7afac0350b1e..ba027017a1cd 100644
--- a/.github/actions/gn/action.yaml
+++ b/.github/actions/gn/action.yaml
@@ -22,13 +22,13 @@ runs:
run: |
set -x
extra_arguments="${{matrix.extra_gn_arguments}}"
- if [ -z ${COBALT_BOOTLOADER+x} ]; then
+ if [ -z ${COBALT_EVERGREEN_LOADER+x} ]; then
BUILD_PLATFORM=${{ matrix.target_platform }}
else
- BUILD_PLATFORM=${COBALT_BOOTLOADER}
- if [ ! -z "${{matrix.bootloader_extra_gn_arguments}}" ]
+ BUILD_PLATFORM=${COBALT_EVERGREEN_LOADER}
+ if [ ! -z "${{matrix.evergreen_loader_extra_gn_arguments}}" ]
then
- extra_arguments="${{matrix.bootloader_extra_gn_arguments}}"
+ extra_arguments="${{matrix.evergreen_loader_extra_gn_arguments}}"
fi
fi
gn gen $GITHUB_WORKSPACE/out/${BUILD_PLATFORM}_${{matrix.config}} --args="target_platform=\"${BUILD_PLATFORM}\" ${{matrix.sb_api_version}} ${{matrix.target_os}} ${{matrix.target_cpu}} ${extra_arguments} is_internal_build=false build_type=\"${{matrix.config}}\""
diff --git a/.github/actions/on_device_tests/action.yaml b/.github/actions/on_device_tests/action.yaml
index 7b9ef1e98983..c981288dafe7 100644
--- a/.github/actions/on_device_tests/action.yaml
+++ b/.github/actions/on_device_tests/action.yaml
@@ -23,9 +23,9 @@ runs:
echo "WORKFLOW=${WORKFLOW}" >> $GITHUB_ENV
# Boot loader env
- if [ "${COBALT_BOOTLOADER}" != "null" ]; then
+ if [ "${COBALT_EVERGREEN_LOADER}" != "null" ]; then
echo "LOADER_CONFIG=${{ matrix.config }}" >> $GITHUB_ENV
- echo "LOADER_PLATFORM=${COBALT_BOOTLOADER}" >> $GITHUB_ENV
+ echo "LOADER_PLATFORM=${COBALT_EVERGREEN_LOADER}" >> $GITHUB_ENV
fi
# Dimension env
diff --git a/.github/actions/on_host_test/action.yaml b/.github/actions/on_host_test/action.yaml
index a10a8bf2af5d..c751de0de0f9 100644
--- a/.github/actions/on_host_test/action.yaml
+++ b/.github/actions/on_host_test/action.yaml
@@ -39,21 +39,21 @@ runs:
fi
python3 ${GITHUB_WORKSPACE}/tools/create_archive.py -x -s ${GITHUB_WORKSPACE}/out/tmp/${{matrix.platform}}_${{matrix.config}}.${ARCHIVE_EXTENSION} -d ${GITHUB_WORKSPACE}/out ${parallel}
rm -rf ${GITHUB_WORKSPACE}/out/tmp
- - name: Download Bootloader Archive
- if: ${{ env.COBALT_BOOTLOADER != null && env.COBALT_BOOTLOADER != 'null' }}
+ - name: Download Evergreen loader Archive
+ if: ${{ env.COBALT_EVERGREEN_LOADER != null && env.COBALT_EVERGREEN_LOADER != 'null' }}
shell: bash
env:
WORKFLOW: ${{ github.workflow }}
run: |
set -x
PROJECT_NAME=$(gcloud config get-value project)
- gsutil cp gs://${PROJECT_NAME}-test-artifacts/${WORKFLOW}/${GITHUB_RUN_NUMBER}/${{matrix.platform}}_${{matrix.config}}/${COBALT_BOOTLOADER}_${{matrix.config}}.${ARCHIVE_EXTENSION} ${GITHUB_WORKSPACE}/out/tmp/${COBALT_BOOTLOADER}_${{matrix.config}}.${ARCHIVE_EXTENSION}
- - name: Extract Bootloader Archive
- if: ${{ env.COBALT_BOOTLOADER != null && env.COBALT_BOOTLOADER != 'null' }}
+ gsutil cp gs://${PROJECT_NAME}-test-artifacts/${WORKFLOW}/${GITHUB_RUN_NUMBER}/${{matrix.platform}}_${{matrix.config}}/${COBALT_EVERGREEN_LOADER}_${{matrix.config}}.${ARCHIVE_EXTENSION} ${GITHUB_WORKSPACE}/out/tmp/${COBALT_EVERGREEN_LOADER}_${{matrix.config}}.${ARCHIVE_EXTENSION}
+ - name: Extract Evergreen loader Archive
+ if: ${{ env.COBALT_EVERGREEN_LOADER != null && env.COBALT_EVERGREEN_LOADER != 'null' }}
shell: bash
run: |
set -x
- python3 ${GITHUB_WORKSPACE}/tools/create_archive.py -x -s ${GITHUB_WORKSPACE}/out/tmp/${COBALT_BOOTLOADER}_${{matrix.config}}.${ARCHIVE_EXTENSION} -d ${GITHUB_WORKSPACE}/out --parallel
+ python3 ${GITHUB_WORKSPACE}/tools/create_archive.py -x -s ${GITHUB_WORKSPACE}/out/tmp/${COBALT_EVERGREEN_LOADER}_${{matrix.config}}.${ARCHIVE_EXTENSION} -d ${GITHUB_WORKSPACE}/out --parallel
rm -rf ${GITHUB_WORKSPACE}/out/tmp
- name: Set Env Variables
shell: bash
@@ -66,8 +66,8 @@ runs:
run: |
set -x
loader_args=''
- if [ "${COBALT_BOOTLOADER}" != "null" ]; then
- loader_args="--loader_platform ${COBALT_BOOTLOADER} --loader_config ${{matrix.config}}"
+ if [ "${COBALT_EVERGREEN_LOADER}" != "null" ]; then
+ loader_args="--loader_platform ${COBALT_EVERGREEN_LOADER} --loader_config ${{matrix.config}}"
fi
if [[ "${{matrix.shard}}" == 'integration' ]]; then
xvfb-run -a --server-args="-screen 0 1920x1080x24i +render +extension GLX -noreset" python3 $GITHUB_WORKSPACE/cobalt/black_box_tests/black_box_tests.py --platform ${{matrix.target_platform}} --config ${{matrix.config}} ${loader_args}
diff --git a/.github/actions/upload_test_artifacts/action.yaml b/.github/actions/upload_test_artifacts/action.yaml
index d2923eb553a7..33eaa063fc0f 100644
--- a/.github/actions/upload_test_artifacts/action.yaml
+++ b/.github/actions/upload_test_artifacts/action.yaml
@@ -18,13 +18,13 @@ runs:
run: |
set -x
project_name=$(gcloud config get-value project)
- if [ -z ${COBALT_BOOTLOADER+x} ]
+ if [ -z ${COBALT_EVERGREEN_LOADER+x} ]
then
PLATFORM=${{matrix.platform}}
echo "TARGET_PLATFORM=${{matrix.target_platform}}" >> $GITHUB_ENV
else
- PLATFORM=${COBALT_BOOTLOADER}
- echo "TARGET_PLATFORM=${COBALT_BOOTLOADER}" >> $GITHUB_ENV
+ PLATFORM=${COBALT_EVERGREEN_LOADER}
+ echo "TARGET_PLATFORM=${COBALT_EVERGREEN_LOADER}" >> $GITHUB_ENV
fi
if [ "${{ inputs.type }}" == 'ondevice' ]
@@ -56,9 +56,9 @@ runs:
if [ "${{ inputs.type }}" == 'ondevice' ]
then
outdir="$GITHUB_WORKSPACE/out/${{matrix.target_platform}}_${{matrix.config}}"
- if [ -n "${COBALT_BOOTLOADER}" ]
+ if [ -n "${COBALT_EVERGREEN_LOADER}" ]
then
- outdir="${outdir} $GITHUB_WORKSPACE/out/${COBALT_BOOTLOADER}_${{matrix.config}}"
+ outdir="${outdir} $GITHUB_WORKSPACE/out/${COBALT_EVERGREEN_LOADER}_${{matrix.config}}"
fi
python3 $GITHUB_WORKSPACE/tools/create_archive.py --test_infra -d ${{env.ARCHIVE_FILE}} -s ${outdir}
elif [ "${{ inputs.type }}" == 'onhost' ]
diff --git a/.github/config/evergreen-arm-hardfp.json b/.github/config/evergreen-arm-hardfp.json
index 8067c723440a..13b3cd366bb8 100644
--- a/.github/config/evergreen-arm-hardfp.json
+++ b/.github/config/evergreen-arm-hardfp.json
@@ -1,6 +1,6 @@
{
"docker_service": "build-raspi",
- "bootloader": "raspi-2",
+ "evergreen_loader": "raspi-2",
"on_device_test": {
"enabled": true,
"tests": [
@@ -25,7 +25,7 @@
"target_platform":"evergreen-arm-hardfp",
"target_cpu":"target_cpu=\\\"arm\\\"",
"extra_gn_arguments":"use_asan=false",
- "bootloader_extra_gn_arguments": "use_asan=false is_clang=false",
+ "evergreen_loader_extra_gn_arguments": "use_asan=false is_clang=false",
"dimension": "release_version=regex:10.*"
},
{
@@ -34,7 +34,7 @@
"target_platform":"evergreen-arm-hardfp",
"target_cpu":"target_cpu=\\\"arm\\\"",
"extra_gn_arguments":"use_asan=false",
- "bootloader_extra_gn_arguments":"use_asan=false is_clang=false",
+ "evergreen_loader_extra_gn_arguments":"use_asan=false is_clang=false",
"sb_api_version": "sb_api_version=15",
"dimension": "release_version=regex:10.*"
},
@@ -44,7 +44,7 @@
"target_platform":"evergreen-arm-hardfp",
"target_cpu":"target_cpu=\\\"arm\\\"",
"extra_gn_arguments":"use_asan=false",
- "bootloader_extra_gn_arguments":"use_asan=false is_clang=false",
+ "evergreen_loader_extra_gn_arguments":"use_asan=false is_clang=false",
"sb_api_version": "sb_api_version=14",
"dimension": "release_version=regex:10.*"
},
@@ -54,7 +54,7 @@
"target_platform":"evergreen-arm-hardfp",
"target_cpu":"target_cpu=\\\"arm\\\"",
"extra_gn_arguments":"use_asan=false",
- "bootloader_extra_gn_arguments":"use_asan=false is_clang=false",
+ "evergreen_loader_extra_gn_arguments":"use_asan=false is_clang=false",
"sb_api_version": "sb_api_version=13",
"dimension": "release_version=regex:10.*"
}
diff --git a/.github/config/evergreen-x64.json b/.github/config/evergreen-x64.json
index b95a3183741a..0f18b571e021 100644
--- a/.github/config/evergreen-x64.json
+++ b/.github/config/evergreen-x64.json
@@ -1,7 +1,7 @@
{
"docker_service": "build-linux-evergreen",
"on_host_test": true,
- "bootloader": "linux-x64x11",
+ "evergreen_loader": "linux-x64x11",
"on_host_test_shards": ["0", "1", "2", "3", "blackbox", "wpt"],
"platforms": [
"evergreen-x64",
diff --git a/.github/config/linux.json b/.github/config/linux.json
index 79ceb0bbd36a..e1efe4b1c102 100644
--- a/.github/config/linux.json
+++ b/.github/config/linux.json
@@ -1,5 +1,6 @@
{
"docker_service": "build-linux",
+ "evergreen_loader": "linux-x64x11",
"on_host_test": true,
"on_host_test_shards": ["0", "1", "2", "3", "blackbox", "wpt"],
"platforms": [
diff --git a/.github/workflows/android_24.lts.1+.yaml b/.github/workflows/android_24.lts.1+.yaml
index cec9f87e4c68..18e7546ca54b 100644
--- a/.github/workflows/android_24.lts.1+.yaml
+++ b/.github/workflows/android_24.lts.1+.yaml
@@ -1,6 +1,8 @@
name: android_24.lts.1+
on:
+ release:
+ types: [prereleased, released, published]
pull_request:
types: [ready_for_review, opened, reopened, synchronize, labeled]
branches:
@@ -28,6 +30,7 @@ jobs:
with:
platform: android-arm64
nightly: ${{ github.event.inputs.nightly }}
+ keep_artifacts: cobalt.apk
android-x86:
uses: ./.github/workflows/main.yaml
permissions:
@@ -36,6 +39,7 @@ jobs:
with:
platform: android-x86
nightly: ${{ github.event.inputs.nightly }}
+ keep_artifacts: cobalt.apk
android-arm:
uses: ./.github/workflows/main.yaml
permissions:
@@ -44,3 +48,49 @@ jobs:
with:
platform: android-arm
nightly: ${{ github.event.inputs.nightly }}
+ keep_artifacts: cobalt.apk
+
+ upload-release-artifacts:
+ runs-on: ubuntu-latest
+ needs: [ android-arm, android-arm64, android-x86 ]
+ permissions:
+ actions: write
+ steps:
+ - name: Download arm-gold apk
+ uses: actions/download-artifact@v4
+ with:
+ name: android-arm-gold
+ path: arm-gold
+ - name: Download arm-qa apk
+ uses: actions/download-artifact@v4
+ with:
+ name: android-arm-qa
+ path: arm-qa
+ - name: Download arm64-gold apk
+ uses: actions/download-artifact@v4
+ with:
+ name: android-arm64-gold
+ path: arm64-gold
+ - name: Download arm64-qa apk
+ uses: actions/download-artifact@v4
+ with:
+ name: android-arm64-gold
+ path: arm64-qa
+ - name: Download x86-gold apk
+ uses: actions/download-artifact@v4
+ with:
+ name: android-x86-gold
+ path: x86-gold
+ - name: Download x86-qa apk
+ uses: actions/download-artifact@v4
+ with:
+ name: android-x86-qa
+ path: x86-qa
+ - name: 'Upload Android APKs'
+ uses: actions/upload-artifact@v4
+ with:
+ name: Android APKs
+ path: ./*
+ retention-days: 90
+ compression-level: 0 # We expect kept artifacts to be already compressed
+ if-no-files-found: error
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 9ccebb8620df..4717d45d8a5e 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -29,6 +29,11 @@ on:
required: false
type: boolean
default: false
+ keep_artifacts:
+ description: 'Which artifacts to keep for releases'
+ required: false
+ type: string
+ default: ''
# Global env vars.
env:
@@ -100,8 +105,8 @@ jobs:
run: echo "on_host_test=$(cat ${GITHUB_WORKSPACE}/.github/config/${{ inputs.platform }}.json | jq -rc '.on_host_test')" >> $GITHUB_ENV
- id: set-on-host-test-shards
run: echo "on_host_test_shards=$(cat ${GITHUB_WORKSPACE}/.github/config/${{ inputs.platform }}.json | jq -c '.on_host_test_shards')" >> $GITHUB_ENV
- - id: set-on-host-test-bootloader
- run: echo "bootloader=$(cat ${GITHUB_WORKSPACE}/.github/config/${{ inputs.platform }}.json | jq -rc '.bootloader')" >> $GITHUB_ENV
+ - id: set-on-host-test-evergreen-loader
+ run: echo "evergreen_loader=$(cat ${GITHUB_WORKSPACE}/.github/config/${{ inputs.platform }}.json | jq -rc '.evergreen_loader')" >> $GITHUB_ENV
- id: set-docker-service
run: |
echo "docker_service=$(cat ${GITHUB_WORKSPACE}/.github/config/${{ inputs.platform }}.json | jq -r '.docker_service')" >> $GITHUB_ENV
@@ -113,7 +118,7 @@ jobs:
on_device_test_attempts: ${{ env.on_device_test_attempts }}
on_host_test: ${{ env.on_host_test }}
on_host_test_shards: ${{ env.on_host_test_shards }}
- bootloader: ${{ env.bootloader }}
+ evergreen_loader: ${{ env.evergreen_loader }}
docker_service: ${{ env.docker_service }}
# Builds, tags, and pushes Cobalt docker build images to ghr.
@@ -210,34 +215,43 @@ jobs:
uses: ./.github/actions/gn
- name: Build Cobalt
uses: ./.github/actions/build
+ - name: 'Upload Artifact'
+ uses: actions/upload-artifact@v4
+ if: inputs.keep_artifacts
+ with:
+ name: ${{ matrix.platform }}-${{ matrix.config }}
+ path: out/${{ matrix.platform }}_${{ matrix.config }}/${{ inputs.keep_artifacts }}
+ retention-days: 7
+ compression-level: 0 # We expect kept artifacts to be already compressed
+ if-no-files-found: error
- name: Run API Leak Detector
uses: ./.github/actions/api_leak_detector
if: inputs.run_api_leak_detector
with:
relative_manifest_path: ${{ inputs.leak_manifest_filename }}
- - name: Upload Nightly Artifacts
- if: ${{ ( inputs.nightly == 'true' || github.event_name == 'schedule' ) && matrix.config != 'debug' }}
- uses: ./.github/actions/upload_nightly_artifacts
- name: Upload On Host Test Artifacts
if: ${{ matrix.config == 'devel' && needs.initialize.outputs.on_host_test == 'true' }}
uses: ./.github/actions/upload_test_artifacts
with:
type: onhost
os: linux
- # For some reason passing needs.initialize.outputs.bootloader as parameter to build
+ # For some reason passing needs.initialize.outputs.evergreen_loader as parameter to build
# action didn't work, so instead we set an env var.
- - name: Set bootloader config
- if: ${{ needs.initialize.outputs.bootloader != 'null' }}
- run: echo "COBALT_BOOTLOADER=${{needs.initialize.outputs.bootloader}}" >> $GITHUB_ENV
- # Build bootloader for on-host tests if necessary.
- - name: Bootloader GN
- if: ${{ needs.initialize.outputs.bootloader != 'null' && matrix.config == 'devel' }}
+ - name: Set Evergreen loader config
+ if: ${{ needs.initialize.outputs.evergreen_loader != 'null' }}
+ run: echo "COBALT_EVERGREEN_LOADER=${{needs.initialize.outputs.evergreen_loader}}" >> $GITHUB_ENV
+ # Build Evergreen loader for on-host tests if necessary.
+ - name: Evergreen loader GN
+ if: ${{ needs.initialize.outputs.evergreen_loader != 'null' && ( matrix.config == 'devel' || matrix.config == 'qa' ) }}
uses: ./.github/actions/gn
- - name: Build Bootloader
- if: ${{ needs.initialize.outputs.bootloader != 'null' && matrix.config == 'devel' }}
+ - name: Build Evergreen loader
+ if: ${{ needs.initialize.outputs.evergreen_loader != 'null' && ( matrix.config == 'devel' || matrix.config == 'qa' ) }}
uses: ./.github/actions/build
- - name: Upload Bootloader On Host Test Artifacts
- if: ${{ needs.initialize.outputs.bootloader != 'null' && matrix.config == 'devel' && needs.initialize.outputs.on_host_test == 'true'}}
+ - name: Upload Nightly Artifacts
+ if: ${{ ( inputs.nightly == 'true' || github.event_name == 'schedule' ) && matrix.config != 'debug' }}
+ uses: ./.github/actions/upload_nightly_artifacts
+ - name: Upload Evergreen loader On Host Test Artifacts
+ if: ${{ needs.initialize.outputs.evergreen_loader != 'null' && matrix.config == 'devel' && needs.initialize.outputs.on_host_test == 'true'}}
uses: ./.github/actions/upload_test_artifacts
with:
type: onhost
@@ -279,7 +293,7 @@ jobs:
shard: ${{ fromJson(needs.initialize.outputs.on_device_test).tests }}
include: ${{ fromJson(needs.initialize.outputs.includes) }}
env:
- COBALT_BOOTLOADER: ${{ needs.initialize.outputs.bootloader }}
+ COBALT_EVERGREEN_LOADER: ${{ needs.initialize.outputs.evergreen_loader }}
ON_DEVICE_TEST_ATTEMPTS: ${{ needs.initialize.outputs.on_device_test_attempts }}
MODULAR_BUILD: ${{ inputs.modular && 1 || 0 }}
steps:
@@ -311,7 +325,7 @@ jobs:
# For some reason tests complaining about HOME set to /github/home
# with permission denied error.
HOME: /root
- COBALT_BOOTLOADER: ${{needs.initialize.outputs.bootloader}}
+ COBALT_EVERGREEN_LOADER: ${{needs.initialize.outputs.evergreen_loader}}
MODULAR_BUILD: ${{ inputs.modular && 1 || 0 }}
steps:
- name: Checkout
diff --git a/.gitignore b/.gitignore
index 9a5a5caa9c01..31136b856f33 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@
/venv/*
_certs/
.coverage
+compile_commands.json
diff --git a/cobalt/base/tokens.h b/cobalt/base/tokens.h
index 0b82b64e8116..908ce2d0a17e 100644
--- a/cobalt/base/tokens.h
+++ b/cobalt/base/tokens.h
@@ -116,6 +116,7 @@ namespace base {
MacroOpWithNameOnly(resourcetimingbufferfull) \
MacroOpWithNameOnly(result) \
MacroOpWithNameOnly(resume) \
+ MacroOpWithNameOnly(samplebufferfull) \
MacroOpWithNameOnly(scroll) \
MacroOpWithNameOnly(securitypolicyviolation) \
MacroOpWithNameOnly(seeked) \
diff --git a/cobalt/black_box_tests/black_box_tests.py b/cobalt/black_box_tests/black_box_tests.py
index 7dc6fd7ab359..98430bda0f22 100755
--- a/cobalt/black_box_tests/black_box_tests.py
+++ b/cobalt/black_box_tests/black_box_tests.py
@@ -78,6 +78,7 @@
'disable_eval_with_csp',
'h5vcc_storage_write_verify_test',
'http_cache',
+ 'javascript_profiler',
'persistent_cookie',
'scroll',
'service_worker_add_to_cache_test',
diff --git a/cobalt/black_box_tests/testdata/javascript_profiler.html b/cobalt/black_box_tests/testdata/javascript_profiler.html
new file mode 100644
index 000000000000..f268510db49a
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/javascript_profiler.html
@@ -0,0 +1,88 @@
+
+
+
+ JavaScript Profiler Test
+
+
+
+
+
+
diff --git a/cobalt/black_box_tests/tests/javascript_profiler.py b/cobalt/black_box_tests/tests/javascript_profiler.py
new file mode 100644
index 000000000000..33dabc7a0821
--- /dev/null
+++ b/cobalt/black_box_tests/tests/javascript_profiler.py
@@ -0,0 +1,40 @@
+# Copyright 2024 The Cobalt 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.
+"""Tests if Cobalt client page can use window.Profiler."""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class JavaScriptProfilerTest(black_box_tests.BlackBoxTestCase):
+ """Ensure that the client can declare a `window.Profiler` object."""
+
+ def test_javascript_profiler_prime_profiler(self):
+ with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+ modes = [
+ 'testPrimeProfiler',
+ 'testSampleBufferFullProfiler',
+ 'testAbruptGarbageCollection',
+ ]
+ for mode in modes:
+ url = server.GetURL(
+ file_name=f'testdata/javascript_profiler.html?mode={mode}')
+ with self.CreateCobaltRunner(url=url) as runner:
+ runner.WaitForJSTestsSetup()
+ self.assertTrue(runner.JSTestsSucceeded(),
+ f'JavaScript profiler failed at case mode="{mode}".')
diff --git a/cobalt/dom/html_media_element.cc b/cobalt/dom/html_media_element.cc
index 45a49ca18f6c..ade06946cc33 100644
--- a/cobalt/dom/html_media_element.cc
+++ b/cobalt/dom/html_media_element.cc
@@ -644,21 +644,16 @@ void HTMLMediaElement::ScheduleEvent(const scoped_refptr& event) {
event_queue_.Enqueue(event);
}
-std::string HTMLMediaElement::h5vcc_audio_connectors(
- script::ExceptionState* exception_state) const {
+std::string HTMLMediaElement::h5vcc_audio_connectors() const {
#if SB_API_VERSION >= 15
if (!player_) {
- web::DOMException::Raise(web::DOMException::kInvalidStateErr,
- exception_state);
- return std::string();
+ return "";
}
std::vector configs = player_->GetAudioConnectors();
return base::JoinString(configs, ";");
#else // SB_API_VERSION >= 15
- web::DOMException::Raise(web::DOMException::kNotSupportedErr,
- exception_state);
- return std::string();
+ return "";
#endif // SB_API_VERSION >= 15
}
diff --git a/cobalt/dom/html_media_element.h b/cobalt/dom/html_media_element.h
index 559c72a0ab60..f4eefebf3a1e 100644
--- a/cobalt/dom/html_media_element.h
+++ b/cobalt/dom/html_media_element.h
@@ -149,8 +149,7 @@ class HTMLMediaElement : public HTMLElement,
// Returns semicolon separated names of audio connectors, like
// "hdmi;bluetooth".
// TODO(b/267678497): The current interface is tentative, to be refined.
- std::string h5vcc_audio_connectors(
- script::ExceptionState* exception_state) const;
+ std::string h5vcc_audio_connectors() const;
// Set max video capabilities.
void SetMaxVideoCapabilities(const std::string& max_video_capabilities,
diff --git a/cobalt/dom/html_media_element.idl b/cobalt/dom/html_media_element.idl
index 401060a6f7b7..f3b79173e6c6 100644
--- a/cobalt/dom/html_media_element.idl
+++ b/cobalt/dom/html_media_element.idl
@@ -63,8 +63,7 @@ interface HTMLMediaElement : HTMLElement {
attribute boolean muted;
// non standard, semicolon separated names of audio connectors, like
- // "hdmi;bluetooth". It raises `NotSupportedError` on apps doesn't support
- // this feature, or `InvalidStateError` if there isn't an active playback.
+ // "hdmi;bluetooth".
// TODO(b/267678497): The current interface is tentative, to be refined.
- [RaisesException] readonly attribute DOMString h5vccAudioConnectors;
+ readonly attribute DOMString h5vccAudioConnectors;
};
diff --git a/cobalt/js_profiler/BUILD.gn b/cobalt/js_profiler/BUILD.gn
index 2a025d16f9e3..026c22065972 100644
--- a/cobalt/js_profiler/BUILD.gn
+++ b/cobalt/js_profiler/BUILD.gn
@@ -16,6 +16,8 @@ static_library("js_profiler") {
sources = [
"profiler.cc",
"profiler.h",
+ "profiler_group.cc",
+ "profiler_group.h",
"profiler_trace_builder.cc",
"profiler_trace_builder.h",
"profiler_trace_wrapper.h",
diff --git a/cobalt/js_profiler/js_profiler_test.cc b/cobalt/js_profiler/js_profiler_test.cc
index 98bb584a206b..c2c52fa07a92 100644
--- a/cobalt/js_profiler/js_profiler_test.cc
+++ b/cobalt/js_profiler/js_profiler_test.cc
@@ -35,6 +35,10 @@ class ProfilerTest : public dom::testing::TestWithJavaScript {
public:
ProfilerTest() {}
+ void CollectGarbage() {
+ window_.web_context()->javascript_engine()->CollectGarbage();
+ }
+
protected:
dom::testing::StubWindow window_;
StrictMock exception_state_;
@@ -111,5 +115,42 @@ TEST_F(ProfilerTest, ProfilerJSCode) {
EXPECT_TRUE(EvaluateScript("Profiler", &result));
EXPECT_EQ(result, "function Profiler() { [native code] }");
}
+
+TEST_F(ProfilerTest, ProfilerGroupDisposesOfCpuProfiler) {
+ v8::HandleScope scope(web::get_isolate(window_.environment_settings()));
+ ProfilerInitOptions init_options;
+ init_options.set_sample_interval(10);
+ init_options.set_max_buffer_size(1);
+
+ auto profiler_group = ProfilerGroup::From(window_.environment_settings());
+ EXPECT_FALSE(profiler_group->active());
+ EXPECT_EQ(profiler_group->num_active_profilers(), 0);
+
+ scoped_refptr profiler_(new Profiler(
+ window_.environment_settings(), init_options, &exception_state_));
+ EXPECT_EQ(profiler_group->num_active_profilers(), 1);
+ EXPECT_TRUE(profiler_group->active());
+
+ auto promise = profiler_->Stop(window_.environment_settings());
+ EXPECT_EQ(profiler_->stopped(), true);
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_EQ(profiler_group->num_active_profilers(), 0);
+ EXPECT_FALSE(profiler_group->active());
+}
+
+TEST_F(ProfilerTest, ProfilerCanBeCancelled) {
+ v8::HandleScope scope(web::get_isolate(window_.environment_settings()));
+ ProfilerInitOptions init_options;
+ init_options.set_sample_interval(10);
+ init_options.set_max_buffer_size(1000);
+
+ scoped_refptr profiler_(new Profiler(
+ window_.environment_settings(), init_options, &exception_state_));
+
+ profiler_->Cancel();
+ EXPECT_EQ(profiler_->stopped(), true);
+}
+
} // namespace js_profiler
} // namespace cobalt
diff --git a/cobalt/js_profiler/profiler.cc b/cobalt/js_profiler/profiler.cc
index c380aed7c53f..21e4cdddb184 100644
--- a/cobalt/js_profiler/profiler.cc
+++ b/cobalt/js_profiler/profiler.cc
@@ -14,45 +14,27 @@
#include "cobalt/js_profiler/profiler.h"
-#include
#include
#include
#include
#include
-#include "cobalt/base/polymorphic_downcast.h"
-#include "cobalt/js_profiler/profiler_trace_builder.h"
+#include "base/logging.h"
#include "cobalt/js_profiler/profiler_trace_wrapper.h"
#include "cobalt/web/cache_utils.h"
#include "cobalt/web/context.h"
#include "cobalt/web/dom_exception.h"
-#include "cobalt/web/environment_settings.h"
#include "cobalt/web/environment_settings_helper.h"
-namespace {
-v8::Local toV8String(v8::Isolate* isolate,
- const std::string& string) {
- if (string.empty()) return v8::String::Empty(isolate);
- return v8::String::NewFromUtf8(isolate, string.c_str(),
- v8::NewStringType::kNormal, string.length())
- .ToLocalChecked();
-}
-} // namespace
-
namespace cobalt {
namespace js_profiler {
-volatile uint32_t s_lastProfileId = 0;
-
-static constexpr int kBaseSampleIntervalMs = 10;
-
Profiler::Profiler(script::EnvironmentSettings* settings,
ProfilerInitOptions options,
script::ExceptionState* exception_state)
- : cobalt::web::EventTarget(settings),
- stopped_(false),
- time_origin_{base::TimeTicks::Now()} {
- profiler_id_ = nextProfileId();
+ : stopped_(false), time_origin_{base::TimeTicks::Now()} {
+ profiler_group_ = ProfilerGroup::From(settings);
+ profiler_id_ = profiler_group_->NextProfilerId();
const base::TimeDelta sample_interval =
base::Milliseconds(options.sample_interval());
@@ -66,21 +48,19 @@ Profiler::Profiler(script::EnvironmentSettings* settings,
int effective_sample_interval_ms =
static_cast(sample_interval.InMilliseconds());
- if (effective_sample_interval_ms % kBaseSampleIntervalMs != 0 ||
+ if (effective_sample_interval_ms % Profiler::kBaseSampleIntervalMs != 0 ||
effective_sample_interval_ms == 0) {
effective_sample_interval_ms +=
- (kBaseSampleIntervalMs -
- effective_sample_interval_ms % kBaseSampleIntervalMs);
+ (Profiler::kBaseSampleIntervalMs -
+ effective_sample_interval_ms % Profiler::kBaseSampleIntervalMs);
}
sample_interval_ = effective_sample_interval_ms;
- auto isolate = web::get_isolate(settings);
-
- auto status = ImplProfilingStart(
- profiler_id_,
+ SB_LOG(INFO) << "[PROFILER] START " + profiler_id_;
+ auto status = profiler_group_->ProfilerStart(
+ this, settings,
v8::CpuProfilingOptions(v8::kLeafNodeLineNumbers,
- options.max_buffer_size(), sample_interval_us),
- settings);
+ options.max_buffer_size(), sample_interval_us));
if (status == v8::CpuProfilingStatus::kAlreadyStarted) {
web::DOMException::Raise(web::DOMException::kInvalidStateErr,
@@ -91,46 +71,31 @@ Profiler::Profiler(script::EnvironmentSettings* settings,
}
}
-Profiler::~Profiler() {
- if (cpu_profiler_) {
- cpu_profiler_->Dispose();
- cpu_profiler_ = nullptr;
+void Profiler::AddEventListener(
+ script::EnvironmentSettings* environment_settings, const std::string& name,
+ const Profiler::SampleBufferFullCallbackHolder& holder) {
+ DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+ if (name != base::Tokens::samplebufferfull()) {
+ return;
}
+ auto* global_wrappable = web::get_global_wrappable(environment_settings);
+ SampleBufferFullCallbackReference* token_callback =
+ new SampleBufferFullCallbackReference(global_wrappable, holder);
+ listeners_.push_back(
+ std::unique_ptr(token_callback));
}
-v8::CpuProfilingStatus Profiler::ImplProfilingStart(
- std::string profiler_id, v8::CpuProfilingOptions options,
- script::EnvironmentSettings* settings) {
- auto isolate = web::get_isolate(settings);
- cpu_profiler_ = v8::CpuProfiler::New(isolate);
- cpu_profiler_->SetSamplingInterval(kBaseSampleIntervalMs *
- base::Time::kMicrosecondsPerMillisecond);
- return cpu_profiler_->StartProfiling(
- toV8String(isolate, profiler_id), options,
- std::make_unique(this));
-}
-
-std::string Profiler::nextProfileId() {
- s_lastProfileId++;
- return "cobalt::profiler[" + std::to_string(s_lastProfileId) + "]";
-}
-
-void Profiler::PerformStop(
- script::EnvironmentSettings* environment_settings,
- std::unique_ptr promise_reference,
- base::TimeTicks time_origin, std::string profiler_id) {
- auto isolate = web::get_isolate(environment_settings);
- auto profile =
- cpu_profiler_->StopProfiling(toV8String(isolate, profiler_id_));
- auto trace = ProfilerTraceBuilder::FromProfile(profile, time_origin_);
- scoped_refptr result(new ProfilerTraceWrapper(trace));
- cpu_profiler_->Dispose();
- cpu_profiler_ = nullptr;
- promise_reference->value().Resolve(result);
+void Profiler::DispatchSampleBufferFullEvent() {
+ DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+ for (auto it = listeners_.begin(); it != listeners_.end(); ++it) {
+ (*it)->value().Run();
+ }
+ listeners_.clear();
}
Profiler::ProfilerTracePromise Profiler::Stop(
script::EnvironmentSettings* environment_settings) {
+ SB_LOG(INFO) << "[PROFILER] STOPPING " + profiler_id_;
script::HandlePromiseWrappable promise =
web::get_script_value_factory(environment_settings)
->CreateInterfacePromise>();
@@ -145,7 +110,7 @@ Profiler::ProfilerTracePromise Profiler::Stop(
context->message_loop()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&Profiler::PerformStop, base::Unretained(this),
- environment_settings, std::move(promise_reference),
+ profiler_group_, std::move(promise_reference),
std::move(time_origin_), std::move(profiler_id_)));
} else {
promise->Reject(new web::DOMException(web::DOMException::kInvalidStateErr,
@@ -154,5 +119,23 @@ Profiler::ProfilerTracePromise Profiler::Stop(
return promise;
}
+void Profiler::PerformStop(
+ ProfilerGroup* profiler_group,
+ std::unique_ptr promise_reference,
+ base::TimeTicks time_origin, std::string profiler_id) {
+ SB_LOG(INFO) << "[PROFILER] STOPPED " + profiler_id_;
+ auto trace = profiler_group->ProfilerStop(this);
+ scoped_refptr result(new ProfilerTraceWrapper(trace));
+ promise_reference->value().Resolve(result);
+}
+
+void Profiler::Cancel() {
+ if (!stopped_) {
+ stopped_ = true;
+ profiler_group_->ProfilerStop(this);
+ }
+ profiler_group_ = nullptr;
+}
+
} // namespace js_profiler
} // namespace cobalt
diff --git a/cobalt/js_profiler/profiler.h b/cobalt/js_profiler/profiler.h
index a6da1e77e63d..675f60b0a756 100644
--- a/cobalt/js_profiler/profiler.h
+++ b/cobalt/js_profiler/profiler.h
@@ -17,67 +17,78 @@
#include
#include
+#include
+#include "base/threading/thread_checker.h"
#include "cobalt/dom/performance_high_resolution_time.h"
+#include "cobalt/js_profiler/profiler_group.h"
#include "cobalt/js_profiler/profiler_init_options.h"
#include "cobalt/js_profiler/profiler_trace.h"
+#include "cobalt/script/callback_function.h"
#include "cobalt/script/promise.h"
+#include "cobalt/script/script_value.h"
#include "cobalt/script/value_handle.h"
#include "cobalt/script/wrappable.h"
#include "cobalt/web/event_target.h"
-#include "third_party/v8/include/cppgc/member.h"
#include "third_party/v8/include/v8-profiler.h"
namespace cobalt {
namespace js_profiler {
-class Profiler : public cobalt::web::EventTarget {
+// Forward declaration of ProfilerGroup
+class ProfilerGroup;
+
+// TODO(b/326337485): Profiler should be a subclass of EventTarget.
+class Profiler : public script::Wrappable {
public:
- using ProfilerTracePromise = script::HandlePromiseWrappable;
+ static const int kBaseSampleIntervalMs = 10;
+ typedef script::HandlePromiseWrappable ProfilerTracePromise;
+ typedef script::CallbackFunction SampleBufferFullCallback;
+ typedef script::ScriptValue
+ SampleBufferFullCallbackHolder;
+ typedef SampleBufferFullCallbackHolder::Reference
+ SampleBufferFullCallbackReference;
Profiler(script::EnvironmentSettings* settings, ProfilerInitOptions options,
script::ExceptionState* exception_state);
- ~Profiler();
+ ~Profiler() override = default;
+
+ void AddEventListener(script::EnvironmentSettings* environment_settings,
+ const std::string& name,
+ const SampleBufferFullCallbackHolder& listener);
+
+ void DispatchSampleBufferFullEvent();
ProfilerTracePromise Stop(script::EnvironmentSettings* environment_settings);
+ void Cancel();
bool stopped() const { return stopped_; }
dom::DOMHighResTimeStamp sample_interval() const { return sample_interval_; }
+ std::string ProfilerId() const { return profiler_id_; }
+ base::TimeTicks time_origin() const { return time_origin_; }
DEFINE_WRAPPABLE_TYPE(Profiler);
- virtual v8::CpuProfilingStatus ImplProfilingStart(
- std::string profiler_id, v8::CpuProfilingOptions options,
- script::EnvironmentSettings* settings);
-
private:
- void PerformStop(script::EnvironmentSettings* environment_settings,
+ void PerformStop(ProfilerGroup* profiler_group,
std::unique_ptr
promise_reference,
base::TimeTicks time_origin, std::string profiler_id);
- std::string nextProfileId();
-
bool stopped_;
dom::DOMHighResTimeStamp sample_interval_;
- v8::CpuProfiler* cpu_profiler_ = nullptr;
base::TimeTicks time_origin_;
std::string profiler_id_;
-};
+ ProfilerGroup* profiler_group_;
+ // All samplebufferfull listeners. Prevents GC on callbacks owned by this
+ // object, by binding to global-wrappable.
+ std::vector> listeners_;
-class ProfilerMaxSamplesDelegate : public v8::DiscardedSamplesDelegate {
- public:
- explicit ProfilerMaxSamplesDelegate(Profiler* profiler)
- : profiler_(profiler) {}
- void Notify() override {
- if (profiler_.Get()) {
- profiler_->DispatchEvent(new web::Event("samplebufferfull"));
- }
- }
+ // Thread checker for the thread that creates this instance.
+ THREAD_CHECKER(thread_checker_);
- private:
- cppgc::WeakMember profiler_;
+ DISALLOW_COPY_AND_ASSIGN(Profiler);
};
} // namespace js_profiler
diff --git a/cobalt/js_profiler/profiler.idl b/cobalt/js_profiler/profiler.idl
index 6f3793374219..0492b0039aff 100644
--- a/cobalt/js_profiler/profiler.idl
+++ b/cobalt/js_profiler/profiler.idl
@@ -20,9 +20,16 @@
ConstructorCallWith=EnvironmentSettings,
RaisesException = Constructor,
]
-interface Profiler : EventTarget {
+interface Profiler {
readonly attribute DOMHighResTimeStamp sampleInterval;
readonly attribute boolean stopped;
+ // TODO(b/326337485): This function mocks but does not fully emulate the EventTarget interface. It can
+ // take and call many callbacks as listeners. However, note that this class does not remove listeners
+ // or dispatch events. Use with caution.
+ [CallWith=EnvironmentSettings] void addEventListener(DOMString token, SampleBufferFullCallback listener);
+
[CallWith=EnvironmentSettings] Promise stop();
};
+
+callback SampleBufferFullCallback = void();
diff --git a/cobalt/js_profiler/profiler_group.cc b/cobalt/js_profiler/profiler_group.cc
new file mode 100644
index 000000000000..e52512dc561c
--- /dev/null
+++ b/cobalt/js_profiler/profiler_group.cc
@@ -0,0 +1,138 @@
+// Copyright 2024 The Cobalt 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 "cobalt/js_profiler/profiler_group.h"
+
+#include "cobalt/js_profiler/profiler_trace_builder.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/environment_settings_helper.h"
+
+namespace {
+v8::Local toV8String(v8::Isolate* isolate,
+ const std::string& string) {
+ if (string.empty()) return v8::String::Empty(isolate);
+ return v8::String::NewFromUtf8(isolate, string.c_str(),
+ v8::NewStringType::kNormal, string.length())
+ .ToLocalChecked();
+}
+} // namespace
+
+namespace cobalt {
+namespace js_profiler {
+
+ProfilerGroup* ProfilerGroup::From(
+ script::EnvironmentSettings* environment_settings) {
+ web::Context* context = web::get_context(environment_settings);
+ if (!context->profiler_group()) {
+ script::GlobalEnvironment* global_env =
+ web::get_global_environment(environment_settings);
+ context->set_profiler_group(
+ std::make_unique(global_env->isolate()));
+ }
+ return context->profiler_group();
+}
+
+v8::CpuProfilingStatus ProfilerGroup::ProfilerStart(
+ const scoped_refptr& profiler,
+ script::EnvironmentSettings* settings, v8::CpuProfilingOptions options) {
+ if (!cpu_profiler_) {
+ cpu_profiler_ = v8::CpuProfiler::New(isolate_);
+ cpu_profiler_->SetSamplingInterval(
+ cobalt::js_profiler::Profiler::kBaseSampleIntervalMs *
+ base::Time::kMicrosecondsPerMillisecond);
+ }
+ profilers_.push_back(profiler);
+ num_active_profilers_++;
+ return cpu_profiler_->StartProfiling(
+ toV8String(isolate_, profiler->ProfilerId()), options,
+ std::make_unique(this,
+ profiler->ProfilerId()));
+}
+
+ProfilerTrace ProfilerGroup::ProfilerStop(Profiler* profiler) {
+ auto profile = cpu_profiler_->StopProfiling(
+ toV8String(isolate_, profiler->ProfilerId()));
+ this->PopProfiler(profiler->ProfilerId());
+ auto trace =
+ ProfilerTraceBuilder::FromProfile(profile, profiler->time_origin());
+ if (profile) {
+ profile->Delete();
+ }
+ if (cpu_profiler_ && num_active_profilers_ == 0) {
+ cpu_profiler_->Dispose();
+ cpu_profiler_ = nullptr;
+ }
+ return trace;
+}
+
+std::string ProfilerGroup::NextProfilerId() {
+ auto id = "cobalt::profiler[" + std::to_string(next_profiler_id_) + "]";
+ next_profiler_id_++;
+ return id;
+}
+
+void ProfilerGroup::DispatchSampleBufferFullEvent(std::string profiler_id) {
+ auto profiler = GetProfiler(profiler_id);
+
+ if (profiler) {
+ profiler->DispatchSampleBufferFullEvent();
+ }
+}
+
+Profiler* ProfilerGroup::GetProfiler(std::string profiler_id) {
+ auto profiler =
+ std::find_if(profilers_.begin(), profilers_.end(),
+ [&profiler_id](const scoped_refptr& profiler) {
+ return profiler->ProfilerId() == profiler_id;
+ });
+ if (profiler == profilers_.end()) {
+ return nullptr;
+ }
+ return profiler->get();
+}
+
+void ProfilerGroup::PopProfiler(std::string profiler_id) {
+ auto profiler = std::find_if(profilers_.begin(), profilers_.end(),
+ [&profiler_id](const Profiler* profiler) {
+ return profiler->ProfilerId() == profiler_id;
+ });
+ if (profiler != profilers_.end()) {
+ profilers_.erase(profiler);
+ }
+ num_active_profilers_--;
+}
+
+void ProfilerGroup::WillDestroyCurrentMessageLoop() {
+ while (!profilers_.empty()) {
+ Profiler* profiler = profilers_[0];
+ DCHECK(profiler);
+ profiler->Cancel();
+ DCHECK(profiler->stopped());
+ }
+
+ DCHECK_EQ(num_active_profilers_, 0);
+ if (cpu_profiler_) {
+ cpu_profiler_->Dispose();
+ cpu_profiler_ = nullptr;
+ }
+}
+
+void ProfilerMaxSamplesDelegate::Notify() {
+ if (profiler_group_.Get()) {
+ profiler_group_->DispatchSampleBufferFullEvent(profiler_id_);
+ }
+}
+
+} // namespace js_profiler
+} // namespace cobalt
diff --git a/cobalt/js_profiler/profiler_group.h b/cobalt/js_profiler/profiler_group.h
new file mode 100644
index 000000000000..a36a616c5d54
--- /dev/null
+++ b/cobalt/js_profiler/profiler_group.h
@@ -0,0 +1,103 @@
+// Copyright 2024 The Cobalt 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.
+
+#ifndef COBALT_JS_PROFILER_PROFILER_GROUP_H_
+#define COBALT_JS_PROFILER_PROFILER_GROUP_H_
+
+#include