ci/testing: Add MemBrowse Integration

Tracking memory footprint with MemBrowse

Signed-off-by: Michael Rogov Papernov / michael@membrowse.com
This commit is contained in:
Michael Rogov Papernov 2026-04-12 14:20:56 +01:00 committed by Alan C. Assis
parent 8857d3673f
commit b941a715ea
5 changed files with 456 additions and 0 deletions

83
.github/membrowse-targets.json vendored Normal file
View file

@ -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": ""
}
]

42
.github/workflows/membrowse-comment.yml vendored Normal file
View file

@ -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

98
.github/workflows/membrowse-onboard.yml vendored Normal file
View file

@ -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[@]}"

232
.github/workflows/membrowse-report.yml vendored Normal file
View file

@ -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[@]}"

View file

@ -8,6 +8,7 @@
[![Contributors](https://img.shields.io/github/contributors/apache/nuttx)](https://github.com/apache/nuttx/graphs/contributors)
[![GitHub Build Badge](https://github.com/apache/nuttx/workflows/Build/badge.svg)](https://github.com/apache/nuttx/actions/workflows/build.yml)
[![Documentation Badge](https://github.com/apache/nuttx/workflows/Build%20Documentation/badge.svg)](https://nuttx.apache.org/docs/latest/index.html)
[![MemBrowse](https://membrowse.com/badge.svg)](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