This is an automated email from the ASF dual-hosted git repository.
acassis pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nuttx.git
The following commit(s) were added to refs/heads/master by this push:
new b941a715ea3 ci/testing: Add MemBrowse Integration
b941a715ea3 is described below
commit b941a715ea392d1764f87e0e3c391f33c4250126
Author: Michael Rogov Papernov <[email protected]>
AuthorDate: Sun Apr 12 14:20:56 2026 +0100
ci/testing: Add MemBrowse Integration
Tracking memory footprint with MemBrowse
Signed-off-by: Michael Rogov Papernov / [email protected]
---
.github/membrowse-targets.json | 83 ++++++++++++
.github/workflows/membrowse-comment.yml | 42 ++++++
.github/workflows/membrowse-onboard.yml | 98 ++++++++++++++
.github/workflows/membrowse-report.yml | 232 ++++++++++++++++++++++++++++++++
README.md | 1 +
5 files changed, 456 insertions(+)
diff --git a/.github/membrowse-targets.json b/.github/membrowse-targets.json
new file mode 100644
index 00000000000..f0a227f4dbb
--- /dev/null
+++ b/.github/membrowse-targets.json
@@ -0,0 +1,83 @@
+[
+ {
+ "target_name": "stm32-nucleo-f103rb",
+ "board_config": "nucleo-f103rb:nsh",
+ "elf": "nuttx",
+ "ld": "boards/arm/stm32/nucleo-f103rb/scripts/ld.script",
+ "map_file": "nuttx.map",
+ "linker_vars": "",
+ "config_overrides": ""
+ },
+ {
+ "target_name": "arduino-mega2560",
+ "board_config": "arduino-mega2560:nsh",
+ "elf": "nuttx.elf",
+ "ld": "boards/avr/atmega/arduino-mega2560/scripts/flash.ld",
+ "map_file": "nuttx.map",
+ "linker_vars": "",
+ "config_overrides": ""
+ },
+ {
+ "target_name": "qemu-armv8a",
+ "board_config": "qemu-armv8a:nsh",
+ "elf": "nuttx",
+ "ld": "",
+ "map_file": "nuttx.map",
+ "linker_vars": "",
+ "config_overrides": ""
+ },
+ {
+ "target_name": "mirtoo",
+ "board_config": "mirtoo:nsh",
+ "elf": "nuttx",
+ "ld": "boards/mips/pic32mx/mirtoo/scripts/pinguino-debug.ld",
+ "map_file": "nuttx.map",
+ "linker_vars": "",
+ "config_overrides": "-d MIPS32_TOOLCHAIN_GNU_ELF -e
MIPS32_TOOLCHAIN_PINGUINOL"
+ },
+ {
+ "target_name": "hifive1-revb",
+ "board_config": "hifive1-revb:nsh",
+ "elf": "nuttx",
+ "ld": "boards/risc-v/fe310/hifive1-revb/scripts/ld.script",
+ "map_file": "nuttx.map",
+ "linker_vars": "",
+ "config_overrides": ""
+ },
+ {
+ "target_name": "rx65n-rsk2mb",
+ "board_config": "rx65n-rsk2mb:nsh",
+ "elf": "nuttx",
+ "ld": "boards/renesas/rx65n/rx65n-rsk2mb/scripts/linker_script.ld",
+ "map_file": "",
+ "linker_vars": "",
+ "config_overrides": ""
+ },
+ {
+ "target_name": "s698pm-dkit",
+ "board_config": "s698pm-dkit:nsh",
+ "elf": "nuttx",
+ "ld": "",
+ "map_file": "",
+ "linker_vars": "",
+ "config_overrides": ""
+ },
+ {
+ "target_name": "qemu-intel64",
+ "board_config": "qemu-intel64:nsh",
+ "elf": "nuttx",
+ "ld": "",
+ "map_file": "nuttx.map",
+ "linker_vars": "",
+ "config_overrides": ""
+ },
+ {
+ "target_name": "esp32-devkitc",
+ "board_config": "esp32-devkitc:nsh",
+ "elf": "nuttx",
+ "ld": "boards/xtensa/esp32/common/scripts/flat_memory.ld.tmp
boards/xtensa/esp32/common/scripts/esp32_sections.ld.tmp",
+ "map_file": "nuttx.map",
+ "linker_vars": "",
+ "config_overrides": ""
+ }
+]
diff --git a/.github/workflows/membrowse-comment.yml
b/.github/workflows/membrowse-comment.yml
new file mode 100644
index 00000000000..05a578a533c
--- /dev/null
+++ b/.github/workflows/membrowse-comment.yml
@@ -0,0 +1,42 @@
+name: MemBrowse PR Comment
+
+on:
+ workflow_run:
+ workflows: [MemBrowse Memory Report]
+ types:
+ - completed
+
+jobs:
+ comment:
+ runs-on: ubuntu-latest
+ if: >
+ github.event.workflow_run.event == 'pull_request' &&
+ github.event.workflow_run.conclusion != 'cancelled'
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+
+ - name: Install membrowse
+ run: pip install --no-cache-dir membrowse
+
+ - name: Post combined PR comment
+ if: ${{ env.MEMBROWSE_API_KEY != '' }}
+ env:
+ MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }}
+ MEMBROWSE_API_URL: ${{ vars.MEMBROWSE_API_URL }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
+ run: |
+ SUMMARY_ARGS=("$HEAD_SHA" --api-key "$MEMBROWSE_API_KEY" --json)
+ if [ -n "$MEMBROWSE_API_URL" ]; then
+ SUMMARY_ARGS+=(--api-url "$MEMBROWSE_API_URL")
+ fi
+ membrowse summary "${SUMMARY_ARGS[@]}" > /tmp/membrowse-summary.json
+ python -m membrowse.utils.github_comment --summary-json
/tmp/membrowse-summary.json
diff --git a/.github/workflows/membrowse-onboard.yml
b/.github/workflows/membrowse-onboard.yml
new file mode 100644
index 00000000000..3ae7af7c421
--- /dev/null
+++ b/.github/workflows/membrowse-onboard.yml
@@ -0,0 +1,98 @@
+name: Onboard to MemBrowse
+
+on:
+ workflow_dispatch:
+ inputs:
+ num_commits:
+ description: 'Number of commits to process'
+ required: true
+ default: '100'
+ type: string
+
+jobs:
+ load-targets:
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ steps:
+ - uses: actions/checkout@v6
+ - id: set-matrix
+ run: echo "matrix=$(jq -c '.' .github/membrowse-targets.json)" >>
$GITHUB_OUTPUT
+
+ onboard:
+ needs: load-targets
+ runs-on: ubuntu-latest
+ container:
+ image: ghcr.io/apache/nuttx/apache-nuttx-ci-linux
+ credentials:
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include: ${{ fromJson(needs.load-targets.outputs.matrix) }}
+
+ steps:
+ - name: Checkout nuttx
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Clone nuttx-apps
+ run: |
+ git config --global --add safe.directory '*'
+ git clone --depth=1 https://github.com/apache/nuttx-apps.git ../apps
+
+ - name: Install membrowse
+ run: python3 -m pip install --no-cache-dir membrowse
+
+ - name: Fetch full git history
+ run: |
+ git fetch --unshallow || true
+ git fetch --all
+
+ - name: Run MemBrowse Onboard
+ env:
+ MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }}
+ MEMBROWSE_API_URL: ${{ vars.MEMBROWSE_API_URL }}
+ NUM_COMMITS: ${{ github.event.inputs.num_commits }}
+ TARGET_NAME: ${{ matrix.target_name }}
+ ELF: ${{ matrix.elf }}
+ LD: ${{ matrix.ld }}
+ MAP_FILE: ${{ matrix.map_file }}
+ LINKER_VARS: ${{ matrix.linker_vars }}
+ run: |
+ BUILD_SCRIPT=$(cat <<'BUILD_EOF'
+ ./tools/configure.sh -l ${{ matrix.board_config }}
+ echo CONFIG_DEBUG_SYMBOLS=y >> .config
+ echo CONFIG_DEBUG_LINK_MAP=y >> .config
+ if [ -n "${{ matrix.config_overrides }}" ]; then
+ kconfig-tweak ${{ matrix.config_overrides }}
+ fi
+ make olddefconfig
+ find arch -name Makefile -exec sed -i '/DELFILE, $(addsuffix
.tmp,$(ARCHSCRIPT))/d' {} +
+ trap 'git checkout -- arch/' EXIT
+ make -j$(nproc)
+ BUILD_EOF
+ )
+
+ ARGS=("$NUM_COMMITS" "$BUILD_SCRIPT" "$ELF" "$TARGET_NAME"
"$MEMBROWSE_API_KEY")
+ if [ -n "$MEMBROWSE_API_URL" ]; then
+ ARGS+=(--api-url "$MEMBROWSE_API_URL")
+ fi
+ if [ -n "$LD" ]; then
+ ARGS+=(--ld-scripts "$LD")
+ fi
+ if [ -n "$LINKER_VARS" ]; then
+ set -f
+ for var in $LINKER_VARS; do
+ ARGS+=(--def "$var")
+ done
+ set +f
+ fi
+ if [ -n "$MAP_FILE" ]; then
+ ARGS+=(--map-file "$MAP_FILE")
+ fi
+ ARGS+=(--binary-search)
+
+ membrowse onboard "${ARGS[@]}"
diff --git a/.github/workflows/membrowse-report.yml
b/.github/workflows/membrowse-report.yml
new file mode 100644
index 00000000000..04e61f57541
--- /dev/null
+++ b/.github/workflows/membrowse-report.yml
@@ -0,0 +1,232 @@
+name: MemBrowse Memory Report
+
+on:
+ pull_request:
+ push:
+ branches:
+ - master
+ - "releases/*"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number ||
github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ # Detect whether any source files (i.e. non-doc/CODEOWNERS) changed.
+ # When only docs/CODEOWNERS change we skip the build and post an "identical"
+ # MemBrowse report instead.
+ changes-filter:
+ runs-on: ubuntu-latest
+ outputs:
+ source: ${{ steps.filter.outputs.source }}
+ steps:
+ # Shallow checkout; the base commit needed for the diff is fetched
+ # explicitly below instead of cloning the repo's full history.
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 2
+ # Detect whether any non-doc/CODEOWNERS source files changed.
+ # Implemented with plain git instead of a third-party paths-filter
+ # action, since Apache's GitHub Actions allowlist forbids actions that
+ # aren't GitHub-created or explicitly permitted.
+ - id: filter
+ env:
+ BASE_SHA: ${{ github.event_name == 'pull_request' &&
github.event.pull_request.base.sha || github.event.before }}
+ HEAD_SHA: ${{ github.event_name == 'pull_request' &&
github.event.pull_request.head.sha || github.sha }}
+ run: |
+ # On the very first push to a branch, github.event.before is
all-zeros.
+ if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" =
"0000000000000000000000000000000000000000" ]; then
+ echo "source=true" >> "$GITHUB_OUTPUT"
+ echo "No usable base SHA; treating as source change."
+ exit 0
+ fi
+ # The shallow checkout may not contain BASE_SHA (multi-commit pushes,
+ # long-lived PR branches). Fetch just that commit; fall back to a
full
+ # unshallow only if the targeted fetch fails.
+ if ! git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then
+ git fetch --depth=1 origin "$BASE_SHA" 2>/dev/null \
+ || git fetch --unshallow origin 2>/dev/null \
+ || git fetch --unshallow 2>/dev/null || true
+ fi
+ changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
+ echo "Changed files:"
+ echo "$changed"
+ # Drop paths that should NOT count as source changes; if anything
+ # survives, real source changed.
+ source=$(echo "$changed" | grep -vE \
+ -e '^AUTHORS$' \
+ -e '^CONTRIBUTING\.md$' \
+ -e '(^|/)CODEOWNERS$' \
+ -e '^Documentation/' \
+ -e '^tools/ci/docker/linux/' \
+ -e '^tools/codeowners/[^/]+$' \
+ || true)
+ if [ -n "$source" ]; then
+ echo "source=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "source=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ load-targets:
+ runs-on: ubuntu-latest
+ outputs:
+ matrix: ${{ steps.set-matrix.outputs.matrix }}
+ steps:
+ - uses: actions/checkout@v6
+ - id: set-matrix
+ run: echo "matrix=$(jq -c '.' .github/membrowse-targets.json)" >>
$GITHUB_OUTPUT
+
+ # Post an "identical" MemBrowse report when only docs/CODEOWNERS changed.
+ # Only runs on push events (master/releases/tags) to keep the baseline
+ # complete; PR events don't produce identical markers.
+ identical:
+ needs: [changes-filter, load-targets]
+ if: needs.changes-filter.outputs.source == 'false' && github.event_name ==
'push'
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include: ${{ fromJson(needs.load-targets.outputs.matrix) }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 2
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+ - name: Install membrowse
+ run: pip install --no-cache-dir membrowse
+ - name: Upload identical - ${{ matrix.target_name }}
+ env:
+ MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }}
+ MEMBROWSE_API_URL: ${{ vars.MEMBROWSE_API_URL }}
+ TARGET_NAME: ${{ matrix.target_name }}
+ run: |
+ ARGS=(--upload --github
+ --target-name "$TARGET_NAME"
+ --api-key "$MEMBROWSE_API_KEY"
+ --identical)
+ if [ -n "$MEMBROWSE_API_URL" ]; then
+ ARGS+=(--api-url "$MEMBROWSE_API_URL")
+ fi
+ membrowse report "${ARGS[@]}"
+
+ # Build target with debug symbols + linker map, then upload MemBrowse report.
+ # Uses the NuttX CI Docker image so toolchains, kconfig-frontends, etc. are
+ # already installed.
+ analyze:
+ needs: [changes-filter, load-targets]
+ if: needs.changes-filter.outputs.source == 'true'
+ runs-on: ubuntu-latest
+ env:
+ DOCKER_BUILDKIT: 1
+ strategy:
+ fail-fast: false
+ matrix:
+ include: ${{ fromJson(needs.load-targets.outputs.matrix) }}
+ steps:
+ - name: Checkout nuttx repo
+ uses: actions/checkout@v6
+ with:
+ path: sources/nuttx
+ fetch-depth: 2
+
+ - name: Checkout nuttx-apps repo
+ uses: actions/checkout@v6
+ with:
+ repository: apache/nuttx-apps
+ path: sources/apps
+ fetch-depth: 1
+
+ - name: Docker Login
+ uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 #
v4.0.0
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Docker Pull
+ run: docker pull ghcr.io/apache/nuttx/apache-nuttx-ci-linux
+
+ - name: Build ${{ matrix.target_name }}
+ uses: ./sources/nuttx/.github/actions/ci-container
+ with:
+ run: |
+ git config --global --add safe.directory
/github/workspace/sources/nuttx
+ git config --global --add safe.directory
/github/workspace/sources/apps
+ cd /github/workspace/sources/nuttx
+ ./tools/configure.sh -l ${{ matrix.board_config }}
+ echo CONFIG_DEBUG_SYMBOLS=y >> .config
+ echo CONFIG_DEBUG_LINK_MAP=y >> .config
+ if [ -n "${{ matrix.config_overrides }}" ]; then
+ kconfig-tweak ${{ matrix.config_overrides }}
+ fi
+ make olddefconfig
+ # Preserve preprocessed linker scripts (.ld.tmp) so MemBrowse can
+ # read them. Arch Makefiles delete these immediately after linking.
+ find arch -name Makefile -exec sed -i '/DELFILE, $(addsuffix
.tmp,$(ARCHSCRIPT))/d' {} +
+ make -j$(nproc)
+
+ # MemBrowse action runs from $GITHUB_WORKSPACE and discovers git via CWD,
+ # but nuttx is checked out to sources/nuttx/ (so apps can sit beside it).
+ # Expose the repo's .git at the workspace root so git metadata resolves.
+ - name: Expose nuttx .git to workspace root
+ run: ln -sfn sources/nuttx/.git .git
+
+ - name: Prefix linker script paths
+ id: ld
+ run: |
+ prefixed=""
+ for p in ${{ matrix.ld }}; do
+ prefixed="$prefixed sources/nuttx/$p"
+ done
+ paths=$(echo $prefixed | xargs)
+ echo "paths=$paths" >> "$GITHUB_OUTPUT"
+ echo "Resolved ld paths: [$paths]"
+ for p in $paths; do
+ if [ -e "$p" ]; then
+ echo " OK $p ($(stat -c '%s bytes, owner=%U:%G' "$p"
2>/dev/null))"
+ else
+ echo " MISS $p"
+ echo " ls dir:"
+ ls -la "$(dirname "$p")" 2>&1 | head -30
+ fi
+ done
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.11'
+ - name: Install membrowse
+ run: pip install --no-cache-dir membrowse
+ - name: MemBrowse analysis - ${{ matrix.target_name }}
+ env:
+ MEMBROWSE_API_KEY: ${{ secrets.MEMBROWSE_API_KEY }}
+ MEMBROWSE_API_URL: ${{ vars.MEMBROWSE_API_URL }}
+ TARGET_NAME: ${{ matrix.target_name }}
+ ELF: sources/nuttx/${{ matrix.elf }}
+ LD_PATHS: ${{ steps.ld.outputs.paths }}
+ MAP_FILE: ${{ matrix.map_file != '' && format('sources/nuttx/{0}',
matrix.map_file) || '' }}
+ LINKER_VARS: ${{ matrix.linker_vars }}
+ run: |
+ set -o pipefail
+ ARGS=("$ELF" "$LD_PATHS"
+ --upload --github
+ --target-name "$TARGET_NAME"
+ --api-key "$MEMBROWSE_API_KEY")
+ if [ -n "$MEMBROWSE_API_URL" ]; then
+ ARGS+=(--api-url "$MEMBROWSE_API_URL")
+ fi
+ if [ -n "$MAP_FILE" ]; then
+ ARGS+=(--map-file "$MAP_FILE")
+ fi
+ if [ -n "$LINKER_VARS" ]; then
+ set -f
+ for var in $LINKER_VARS; do
+ ARGS+=(--def "$var")
+ done
+ set +f
+ fi
+ membrowse --verbose INFO report "${ARGS[@]}"
diff --git a/README.md b/README.md
index 3ec5eba255e..40d6c07114e 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@
[](https://github.com/apache/nuttx/graphs/contributors)
[](https://github.com/apache/nuttx/actions/workflows/build.yml)
[](https://nuttx.apache.org/docs/latest/index.html)
+[](https://membrowse.com/public/apache/nuttx)
Apache NuttX is a real-time operating system (RTOS) with an emphasis on
standards compliance and small footprint. Scalable from 8-bit to 64-bit