[PATCH 00/24] Malloc debugging and test/py improvements
From: Simon Glass <simon.glass@canonical.com> This series adds malloc-debugging features including a traffic log and file output for dumps, along with video optimisations and test/py performance improvements. The overall goal is to speed up pytests and make it easier to debug memory leaks. Changes include: - Sandbox gprof profiling and mcheck runtime-disable support - Video truetype scratch buffer to reduce malloc pressure - Malloc traffic logging with 'malloc log' command - File output for malloc dump and log - test/py performance improvements reducing CPU usage by ~30% Simon Glass (24): u_boot_pylib: command: Convert output before raising exception u_boot_pylib: tout: Add newline parameter to output functions sandbox: Add gprof profiling support sandbox: Add -M option to disable mcheck at runtime Update Claude instructions for uman video: Update stb_truetype video: truetype: Add a scratch buffer to use malloc() less video: Optimise video_flush_copy() for full-line damage test: Rename dm_test_host() sandbox: Increase CONFIG_MCHECK_CALLER_LEN to 80 malloc: Fix malloc_usable_size() to handle mcheck headers test: malloc: Account for mcheck overhead in the large test malloc: Refactor malloc_dump() to use output callback malloc: Add a log for malloc() traffic malloc: Add function to dump the malloc()-traffic log cmd: malloc: Add a command to show the malloc log malloc: Add an option to disable mcheck-backtrace collection malloc: Add file output for heap dump and malloc log doc: malloc: Add a section on finding memory leaks test/py: Handle a failure during configuration test/py: Reduce CPU usage when waiting for console output test/py: Speed up VT100-filtering in expect() test/py: Add an add option to skip flat-tree tests test/py: Add an option to disable the console timeout CLAUDE.md | 12 +- Kconfig | 32 ++ arch/sandbox/config.mk | 4 + arch/sandbox/cpu/start.c | 13 + arch/sandbox/include/asm/state.h | 1 + cmd/Kconfig | 10 + cmd/malloc.c | 41 ++- common/dlmalloc.c | 485 ++++++++++++++++++++++++++++--- config.mk | 4 + configs/sandbox_defconfig | 1 + doc/arch/sandbox/sandbox.rst | 7 + doc/develop/malloc.rst | 115 ++++++++ doc/develop/trace.rst | 40 +++ doc/usage/cmd/font.rst | 9 + doc/usage/cmd/malloc.rst | 30 +- drivers/video/Kconfig | 23 ++ drivers/video/console_truetype.c | 62 +++- drivers/video/stb_truetype.h | 72 ++++- drivers/video/video-uclass.c | 21 +- include/malloc.h | 102 +++++++ include/mcheck.h | 11 + test/cmd/malloc.c | 48 +++ test/common/malloc.c | 111 ++++++- test/dm/host.c | 4 +- test/py/conftest.py | 7 + test/py/console_base.py | 36 ++- test/py/console_sandbox.py | 4 + tools/u_boot_pylib/command.py | 3 +- tools/u_boot_pylib/tout.py | 70 +++-- 29 files changed, 1264 insertions(+), 114 deletions(-) -- 2.43.0 base-commit: 6cff0c8a556d74547b5a24ec9dad0614a8b9d387 branch: extm
From: Simon Glass <simon.glass@canonical.com> Call to_output() before raising CommandExc so that callers catching the exception get string output rather than bytes. This avoids the need for callers to handle bytes decoding themselves. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- tools/u_boot_pylib/command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/u_boot_pylib/command.py b/tools/u_boot_pylib/command.py index cb7ebf49ce5..6b3f9fe59bf 100644 --- a/tools/u_boot_pylib/command.py +++ b/tools/u_boot_pylib/command.py @@ -136,9 +136,10 @@ def run_pipe(pipe_list, infile=None, outfile=None, capture=False, if result.stdout and oneline: result.output = result.stdout.rstrip(b'\r\n') result.return_code = last_pipe.wait() + result = result.to_output(binary) if raise_on_error and result.return_code: raise CommandExc(f"Error running '{user_pipestr}'", result) - return result.to_output(binary) + return result def output(*cmd, **kwargs): -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a newline parameter to all output functions (info, error, warning, notice, detail, debug, user_output, do_output) to allow suppressing the trailing newline. This is useful for progress output where multiple calls should appear on the same line. Example: tout.info('Processing...', newline=False) tout.info('done') Also fix typos in docstrings (msg; -> msg:). Signed-off-by: Simon Glass <simon.glass@canonical.com> --- tools/u_boot_pylib/tout.py | 70 ++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/tools/u_boot_pylib/tout.py b/tools/u_boot_pylib/tout.py index 137b55edfd0..c608806476c 100644 --- a/tools/u_boot_pylib/tout.py +++ b/tools/u_boot_pylib/tout.py @@ -62,98 +62,108 @@ def progress(msg, warning=False, trailer='...'): else: terminal.tprint(_progress) -def _output(level, msg, color=None): +def _output(level, msg, color=None, newline=True): """Output a message to the terminal. Args: level: Verbosity level for this message. It will only be displayed if this as high as the currently selected level. - msg; Message to display. - error: True if this is an error message, else False. + msg: Message to display. + color: Colour to use for the text, None for default. + newline: True to add a newline at the end. """ if verbose >= level: clear_progress() if level <= WARNING: - terminal.tprint(msg, colour=color, col=_color, stderr=True) + terminal.tprint(msg, newline=newline, colour=color, col=_color, + stderr=True) else: - terminal.tprint(msg, colour=color, col=_color) + terminal.tprint(msg, newline=newline, colour=color, col=_color) if level == FATAL: sys.exit(1) -def do_output(level, msg): +def do_output(level, msg, newline=True): """Output a message to the terminal. Args: level: Verbosity level for this message. It will only be displayed if this as high as the currently selected level. - msg; Message to display. + msg: Message to display. + newline: True to add a newline at the end. """ - _output(level, msg) + _output(level, msg, newline=newline) def fatal(msg): """Display an error message and exit Args: - msg; Message to display. + msg: Message to display. """ _output(FATAL, msg, _color.RED) -def error(msg): +def error(msg, newline=True): """Display an error message Args: - msg; Message to display. + msg: Message to display. + newline: True to add a newline at the end. """ - _output(ERROR, msg, _color.RED) + _output(ERROR, msg, _color.RED, newline=newline) -def warning(msg): +def warning(msg, newline=True): """Display a warning message Args: - msg; Message to display. + msg: Message to display. + newline: True to add a newline at the end. """ - _output(WARNING, msg, _color.YELLOW) + _output(WARNING, msg, _color.YELLOW, newline=newline) -def notice(msg): +def notice(msg, newline=True): """Display an important infomation message Args: - msg; Message to display. + msg: Message to display. + newline: True to add a newline at the end. """ - _output(NOTICE, msg) + _output(NOTICE, msg, newline=newline) -def info(msg): +def info(msg, newline=True): """Display an infomation message Args: - msg; Message to display. + msg: Message to display. + newline: True to add a newline at the end. """ - _output(INFO, msg) + _output(INFO, msg, newline=newline) -def detail(msg): +def detail(msg, newline=True): """Display a detailed message Args: - msg; Message to display. + msg: Message to display. + newline: True to add a newline at the end. """ - _output(DETAIL, msg) + _output(DETAIL, msg, newline=newline) -def debug(msg): +def debug(msg, newline=True): """Display a debug message Args: - msg; Message to display. + msg: Message to display. + newline: True to add a newline at the end. """ - _output(DEBUG, msg) + _output(DEBUG, msg, newline=newline) -def user_output(msg): +def user_output(msg, newline=True): """Display a message regardless of the current output level. This is used when the output was specifically requested by the user. Args: - msg; Message to display. + msg: Message to display. + newline: True to add a newline at the end. """ - _output(ERROR, msg) + _output(ERROR, msg, newline=newline) def init(_verbose=WARNING, stdout=sys.stdout, allow_colour=True): """Initialize a new output object. -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a GPROF=1 build option to enable gprof profiling for sandbox. This adds the -pg flag to both compiler and linker when GPROF=1 is set, following the same pattern as the existing FTRACE option. Usage: make GPROF=1 sandbox_defconfig all ./u-boot -T -c "ut dm" ... gprof u-boot gmon.out Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- arch/sandbox/config.mk | 4 ++++ config.mk | 4 ++++ doc/develop/trace.rst | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/arch/sandbox/config.mk b/arch/sandbox/config.mk index b92c259d2e3..92bac07963e 100644 --- a/arch/sandbox/config.mk +++ b/arch/sandbox/config.mk @@ -10,6 +10,10 @@ PLATFORM_LIBS += -L$(GCC_LIB_DIR) -lbacktrace endif PLATFORM_LIBS += -lrt +ifdef GPROF +PLATFORM_LIBS += -pg +endif + SDL_CONFIG ?= sdl2-config # Define this to avoid linking with SDL, which requires SDL libraries diff --git a/config.mk b/config.mk index e294be2a4c4..82fdbf3b4f6 100644 --- a/config.mk +++ b/config.mk @@ -63,6 +63,10 @@ ifdef FTRACE PLATFORM_CPPFLAGS += -finstrument-functions -DFTRACE endif +ifdef GPROF +PLATFORM_CPPFLAGS += -pg +endif + ######################################################################### RELFLAGS := $(PLATFORM_RELFLAGS) diff --git a/doc/develop/trace.rst b/doc/develop/trace.rst index d3c8628d124..d3393244658 100644 --- a/doc/develop/trace.rst +++ b/doc/develop/trace.rst @@ -491,6 +491,46 @@ can dramatically increase the size of the trace output as well as the execution time. +Wall-clock Profiling with gprof (Sandbox) +----------------------------------------- + +For sandbox builds, an alternative to U-Boot's internal tracing is to use +the standard GNU gprof profiler. This provides wall-clock profiling with +less overhead than function instrumentation, and produces output that can +be analysed with standard tools. + +To build sandbox with gprof support:: + + make GPROF=1 O=/tmp/b/sandbox sandbox_defconfig + make GPROF=1 O=/tmp/b/sandbox + +Then run U-Boot. A `gmon.out` file is created in the current directory when +the program exits:: + + cd /tmp/b/sandbox + ./u-boot -T -c "ut dm dm_test_rtc_set_get" + +Analyse the results with gprof:: + + gprof u-boot gmon.out | less + +This shows a flat profile (functions sorted by time) and a call graph. The +flat profile shows which functions consume the most CPU time:: + + % cumulative self self total + time seconds seconds calls ms/call ms/call name + 29.41 0.05 0.05 36922 0.00 0.00 memset + 17.65 0.08 0.03 read_uleb128 + 11.76 0.10 0.02 328472 0.00 0.00 string + ... + +The call graph shows the call hierarchy and time spent in each call chain. + +Note that gprof measures CPU time, not wall-clock time, so I/O wait time is +not captured. For boot-time optimisation where I/O is significant, use +bootstage or the internal trace system instead. + + Future Work ----------- -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a command-line option (-M or --no_mcheck) to disable mcheck heap protection at runtime. When mcheck is disabled, the wrapper functions pass through directly to the underlying allocator without adding headers or checking for corruption. This is useful for debugging when mcheck interferes with test results, such as when memory-leak detection reports false positives due to accumulated allocations from other tests. Changes: - Add disable_mcheck flag to sandbox_state - Add mcheck_set_disabled() function to mcheck API - Modify dlmalloc wrappers to bypass mcheck when disabled - Add stub for when MCHECK_HEAP_PROTECTION is not enabled - Document the new option in sandbox.rst Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- arch/sandbox/cpu/start.c | 13 +++++++++++++ arch/sandbox/include/asm/state.h | 1 + common/dlmalloc.c | 33 +++++++++++++++++++++++++++++++- doc/arch/sandbox/sandbox.rst | 7 +++++++ include/mcheck.h | 11 +++++++++++ 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/arch/sandbox/cpu/start.c b/arch/sandbox/cpu/start.c index 1a5ca97e15e..b9f48376d22 100644 --- a/arch/sandbox/cpu/start.c +++ b/arch/sandbox/cpu/start.c @@ -11,6 +11,7 @@ #include <event.h> #include <init.h> #include <log.h> +#include <mcheck.h> #include <os.h> #include <pager.h> #include <sandbox_host.h> @@ -523,6 +524,15 @@ static int sandbox_cmdline_cb_noflat(struct sandbox_state *state, } SANDBOX_CMDLINE_OPT_SHORT(noflat, 'F', 0, "Don't run second set of DM tests"); +static int sandbox_cmdline_cb_no_mcheck(struct sandbox_state *state, + const char *arg) +{ + state->disable_mcheck = true; + + return 0; +} +SANDBOX_CMDLINE_OPT_SHORT(no_mcheck, 'M', 0, "Disable mcheck heap protection"); + static int sandbox_cmdline_cb_soft_fail(struct sandbox_state *state, const char *arg) { @@ -674,6 +684,9 @@ int sandbox_init(int argc, char *argv[], struct global_data *data) if (os_parse_args(state, argc, argv)) return 1; + if (state->disable_mcheck) + mcheck_set_disabled(true); + /* Remove old frame*.bmp files if video_frames_dir is set */ if (state->video_frames_dir) { char pattern[256]; diff --git a/arch/sandbox/include/asm/state.h b/arch/sandbox/include/asm/state.h index ff7493ec5d6..1b6eef4a9c3 100644 --- a/arch/sandbox/include/asm/state.h +++ b/arch/sandbox/include/asm/state.h @@ -178,6 +178,7 @@ struct sandbox_state { bool pager_bypass; /* Enable pager-bypass mode */ bool no_term_present; /* Assume no terminal present */ bool quiet_vidconsole; /* Don't use vidconsole for stdout */ + bool disable_mcheck; /* Disable mcheck heap protection */ int video_test; /* ms to wait before next assert */ const char *video_frames_dir; /* Directory to write video frames */ int video_frame_count; /* Number of frames written */ diff --git a/common/dlmalloc.c b/common/dlmalloc.c index 5069a95d9b3..78efdf5fd9a 100644 --- a/common/dlmalloc.c +++ b/common/dlmalloc.c @@ -5965,6 +5965,14 @@ static bool in_backtrace __section(".data"); */ static bool mcheck_skip_backtrace __section(".data"); +/* Runtime flag to disable mcheck - allows bypassing heap protection */ +static bool mcheck_disabled __section(".data"); + +void mcheck_set_disabled(bool disabled) +{ + mcheck_disabled = disabled; +} + void malloc_backtrace_skip(bool skip) { mcheck_skip_backtrace = skip; @@ -5993,6 +6001,9 @@ void *dlmalloc(size_t bytes) !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) return malloc_simple(bytes); + if (mcheck_disabled) + return dlmalloc_impl(bytes CALLER_NULL); + mcheck_pedantic_prehook(); size_t fullsz = mcheck_alloc_prehook(bytes); void *p = dlmalloc_impl(fullsz CALLER_NULL); @@ -6009,12 +6020,15 @@ void dlfree(void *mem) dlfree_impl(mem); return; } + if (mcheck_disabled) { + dlfree_impl(mem); + return; + } dlfree_impl(mcheck_free_prehook(mem)); } void *dlrealloc(void *oldmem, size_t bytes) { - mcheck_pedantic_prehook(); #ifdef REALLOC_ZERO_BYTES_FREES if (bytes == 0) { if (oldmem) @@ -6026,6 +6040,10 @@ void *dlrealloc(void *oldmem, size_t bytes) if (oldmem == NULL) return dlmalloc(bytes); + if (mcheck_disabled) + return dlrealloc_impl(oldmem, bytes); + + mcheck_pedantic_prehook(); void *p = mcheck_reallocfree_prehook(oldmem); size_t newsz = mcheck_alloc_prehook(bytes); @@ -6041,6 +6059,9 @@ void *dlmemalign(size_t alignment, size_t bytes) !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) return memalign_simple(alignment, bytes); + if (mcheck_disabled) + return dlmemalign_impl(alignment, bytes); + mcheck_pedantic_prehook(); size_t fullsz = mcheck_memalign_prehook(alignment, bytes); void *p = dlmemalign_impl(alignment, fullsz); @@ -6064,6 +6085,9 @@ void *dlcalloc(size_t n, size_t elem_size) return p; } + if (mcheck_disabled) + return dlcalloc_impl(n, elem_size); + mcheck_pedantic_prehook(); /* NB: no overflow check here */ size_t fullsz = mcheck_alloc_prehook(n * elem_size); @@ -6121,6 +6145,13 @@ void *dlcalloc(size_t n, size_t elem_size) } #endif /* MCHECK_HEAP_PROTECTION */ +#if !CONFIG_IS_ENABLED(MCHECK_HEAP_PROTECTION) +/* Stub when mcheck is not enabled */ +void mcheck_set_disabled(bool disabled) +{ +} +#endif + #endif /* !ONLY_MSPACES */ /* ----------------------------- user mspaces ---------------------------- */ diff --git a/doc/arch/sandbox/sandbox.rst b/doc/arch/sandbox/sandbox.rst index 90c3dfa837e..0962aeeb1a0 100644 --- a/doc/arch/sandbox/sandbox.rst +++ b/doc/arch/sandbox/sandbox.rst @@ -181,6 +181,13 @@ available options. Some of these are described below: all log statements at LOGL_DEBUG and below. The higher the number, the more info is shown. +-M, --no_mcheck + Disable mcheck heap protection at runtime. When enabled, the mcheck wrapper + functions pass through directly to the underlying allocator without adding + headers or checking for corruption. This is useful for debugging when mcheck + interferes with test results, such as when memory-leak detection reports false + positives due to accumulated allocations from other tests. + -m, --memory <filename> Sets the location of the file which holds sandbox's emulated RAM. This can be read and written across phases, so that sandbox behaves like a normal board. diff --git a/include/mcheck.h b/include/mcheck.h index b170acf6281..cd72edb6ae8 100644 --- a/include/mcheck.h +++ b/include/mcheck.h @@ -51,4 +51,15 @@ enum mcheck_status mprobe(void *__ptr); /* Called during RAM relocation to reset the heap registry */ void mcheck_on_ramrelocation(size_t offset); +/** + * mcheck_set_disabled() - Disable mcheck at runtime + * + * When disabled, mcheck wrapper functions pass through directly to the + * underlying allocator without adding headers or checking for corruption. + * This is useful for debugging when mcheck interferes with test results. + * + * @disabled: true to disable mcheck, false to enable + */ +void mcheck_set_disabled(bool disabled); + #endif -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Suggest using the new uman tool. Signed-off-by: Simon Glass <simon.glass@canonical.com> --- CLAUDE.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d9e0245cdee..aaec9cc031d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,11 +6,11 @@ This file contains information about building U-Boot for use with Claude Code. ### Using uman (Recommended) -To build U-Boot for sandbox testing, use the `uman` command: +To build U-Boot for sandbox testing, use the `um` command: ```bash # Build for sandbox without LTO (um is a symlink to uman) -um -B sandbox build +um build sandbox [flags] # The -l flag can be used to enable LTO # The build is silent unless there are warnings or errors @@ -40,17 +40,17 @@ There are aliases in ~/bin/git-alias which you can use. To run a sandbox test: ```bash -rtv <test_name> +um test <test_name> # For example: rtv dm_test_video_box -# which translates to: /tmp/b/sandbox/u-boot -v -Tf -c "ut dm video_box" +# similar to: /tmp/b/sandbox/u-boot -v -Tf -c "ut dm video_box" # test output is silenced unless -v is given ``` To run using the Python suite: ```bash -pyt <test_name> -# alias for: test/py/test.py -B sandbox --build-dir /tmp/b/sandbox -k <test_name> +um py -B sandbox <test_name> +# similar to: test/py/test.py -B sandbox --build-dir /tmp/b/sandbox -k <test_name> ``` ## Notes -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> This was last updated in 2023. The updates are minimal but we may as well keep it up to date. Bring in the latest version: f1c79c0 ("Merge pull request #1851 from jeffrbig2/master"_ Signed-off-by: Simon Glass <simon.glass@canonical.com> --- drivers/video/stb_truetype.h | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/drivers/video/stb_truetype.h b/drivers/video/stb_truetype.h index 32a7b6e1dc0..90a5c2e2b3f 100644 --- a/drivers/video/stb_truetype.h +++ b/drivers/video/stb_truetype.h @@ -54,7 +54,7 @@ // Hou Qiming Derek Vinyard // Rob Loach Cort Stratton // Kenney Phillis Jr. Brian Costabile -// Ken Voskuil (kaesve) +// Ken Voskuil (kaesve) Yakov Galka // // VERSION HISTORY // @@ -412,6 +412,7 @@ int main(int arg, char **argv) } #endif + ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////// //// @@ -482,7 +483,7 @@ int main(int arg, char **argv) #endif #ifndef STBTT_memcpy - #include <memory.h> + #include <string.h> #define STBTT_memcpy memcpy #define STBTT_memset memset #endif @@ -563,6 +564,7 @@ STBTT_DEF void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int p STBTT_DEF void stbtt_GetScaledFontVMetrics(const unsigned char *fontdata, int index, float size, float *ascent, float *descent, float *lineGap); // Query the font vertical metrics without having to create a font first. + ////////////////////////////////////////////////////////////////////////////// // // NEW TEXTURE BAKING API @@ -735,6 +737,7 @@ STBTT_DEF int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, in // need to do anything special to free it, because the contents are pure // value data with no additional data structures. Returns 0 on failure. + ////////////////////////////////////////////////////////////////////////////// // // CHARACTER TO GLYPH-INDEX CONVERSIOn @@ -746,6 +749,7 @@ STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codep // codepoint-based functions. // Returns 0 if the character codepoint is not defined in the font. + ////////////////////////////////////////////////////////////////////////////// // // CHARACTER PROPERTIES @@ -915,6 +919,7 @@ STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info STBTT_DEF void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); STBTT_DEF void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); + // @TODO: don't expose this structure typedef struct { @@ -989,6 +994,8 @@ STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, floa // The algorithm has not been optimized at all, so expect it to be slow // if computing lots of characters or very large sizes. + + ////////////////////////////////////////////////////////////////////////////// // // Finding the right font... @@ -1010,6 +1017,7 @@ STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, floa // from the file yourself and do your own comparisons on them. // You have to have called stbtt_InitFont() first. + STBTT_DEF int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags); // returns the offset (not index) of the font that matches, or -1 if none // if you use STBTT_MACSTYLE_DONTCARE, use a font name like "Arial Bold". @@ -2801,6 +2809,7 @@ typedef struct stbtt__edge { int invert; } stbtt__edge; + typedef struct stbtt__active_edge { struct stbtt__active_edge *next; @@ -4595,6 +4604,8 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc scale_y = -scale_y; { + // distance from singular values (in the same units as the pixel grid) + const float eps = 1./1024, eps2 = eps*eps; int x,y,i,j; float *precompute; stbtt_vertex *verts; @@ -4607,15 +4618,15 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; float x1 = verts[j].x*scale_x, y1 = verts[j].y*scale_y; float dist = (float) STBTT_sqrt((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0)); - precompute[i] = (dist == 0) ? 0.0f : 1.0f / dist; + precompute[i] = (dist < eps) ? 0.0f : 1.0f / dist; } else if (verts[i].type == STBTT_vcurve) { float x2 = verts[j].x *scale_x, y2 = verts[j].y *scale_y; float x1 = verts[i].cx*scale_x, y1 = verts[i].cy*scale_y; float x0 = verts[i].x *scale_x, y0 = verts[i].y *scale_y; float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; float len2 = bx*bx + by*by; - if (len2 != 0.0f) - precompute[i] = 1.0f / (bx*bx + by*by); + if (len2 >= eps2) + precompute[i] = 1.0f / len2; else precompute[i] = 0.0f; } else @@ -4680,8 +4691,8 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc float a = 3*(ax*bx + ay*by); float b = 2*(ax*ax + ay*ay) + (mx*bx+my*by); float c = mx*ax+my*ay; - if (a == 0.0) { // if a is 0, it's linear - if (b != 0.0) { + if (STBTT_fabs(a) < eps2) { // if a is 0, it's linear + if (STBTT_fabs(b) >= eps2) { res[num++] = -c/b; } } else { @@ -4961,6 +4972,7 @@ STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const #endif // STB_TRUETYPE_IMPLEMENTATION + // FULL VERSION HISTORY // // 1.25 (2021-07-11) many fixes -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The stb_truetype library performs around 5 allocations per character rendered, totalling approximately 26KB of temporary memory. This creates significant malloc/free overhead and heap fragmentation. Add a scratch buffer mechanism that pre-allocates memory once during probe and reuses it for each character. The buffer is reset at the start of each putc_xy() call, and allocations come from this buffer using a simple bump allocator with 8-byte alignment. If the scratch buffer is exhausted (e.g. for very complex glyphs), the allocator falls back to malloc transparently. The scratch buffer is controlled by two new Kconfig options: - CONSOLE_TRUETYPE_SCRATCH: Enable/disable the feature (default y) - CONSOLE_TRUETYPE_SCRATCH_SIZE: Buffer size in bytes (default 32KB) Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- doc/usage/cmd/font.rst | 9 +++++ drivers/video/Kconfig | 23 ++++++++++++ drivers/video/console_truetype.c | 62 ++++++++++++++++++++++++++++++-- drivers/video/stb_truetype.h | 46 ++++++++++++++++++++++-- 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/doc/usage/cmd/font.rst b/doc/usage/cmd/font.rst index f7a4897667b..a4b9495b977 100644 --- a/doc/usage/cmd/font.rst +++ b/doc/usage/cmd/font.rst @@ -85,6 +85,15 @@ CONFIG_CONSOLE_TRUETYPE_GLYPH_BUF enables a pre-allocated buffer for glyph rendering, avoiding malloc/free per character. The buffer starts at 4KB and grows as needed via realloc(). +CONFIG_CONSOLE_TRUETYPE_SCRATCH enables a scratch buffer for internal stbtt +allocations. Without this, the TrueType library performs around 5 allocations +per character (totalling ~26KB), creating malloc/free overhead and heap +fragmentation. With the scratch buffer, memory is allocated once at probe time +and reused for each character. CONFIG_CONSOLE_TRUETYPE_SCRATCH_SIZE sets the +buffer size (default 32KB), which is sufficient for most Latin characters. +Complex glyphs (CJK, emoji) or very large font sizes may need 64KB or more. +Allocations exceeding the buffer size fall back to malloc transparently. + CONFIG_VIDEO_GLYPH_STATS enables tracking of glyph-rendering statistics. Return value diff --git a/drivers/video/Kconfig b/drivers/video/Kconfig index 0f99ba1845b..4a8090e622d 100644 --- a/drivers/video/Kconfig +++ b/drivers/video/Kconfig @@ -259,6 +259,29 @@ config CONSOLE_TRUETYPE_GLYPH_BUF The buffer starts at 4KB and grows via realloc() as needed to accommodate larger glyphs. +config CONSOLE_TRUETYPE_SCRATCH + bool "TrueType scratch buffer to reduce malloc traffic" + depends on CONSOLE_TRUETYPE + default y + help + Enable a pre-allocated scratch buffer for internal TrueType + rendering allocations. This eliminates malloc/free calls during + character rendering, improving performance and reducing heap + fragmentation. + + With this disabled, stbtt allocates and frees around 26KB of + temporary memory for each character rendered. + +config CONSOLE_TRUETYPE_SCRATCH_SIZE + int "TrueType scratch buffer size" + depends on CONSOLE_TRUETYPE_SCRATCH + default 32768 + help + Size of the scratch buffer in bytes for TrueType rendering. + 32KB is sufficient for most Latin characters. Complex glyphs + (CJK, emoji) may need 64KB or more. Allocations exceeding this + size fall back to malloc. + config VIDEO_GLYPH_STATS bool "Track glyph rendering statistics" depends on CONSOLE_TRUETYPE diff --git a/drivers/video/console_truetype.c b/drivers/video/console_truetype.c index 6e65f55d598..c0574b75fbe 100644 --- a/drivers/video/console_truetype.c +++ b/drivers/video/console_truetype.c @@ -105,8 +105,47 @@ static double tt_acos(double val) #define STBTT_fmod tt_fmod #define STBTT_cos tt_cos #define STBTT_acos tt_acos -#define STBTT_malloc(size, u) ((void)(u), malloc(size)) -#define STBTT_free(size, u) ((void)(u), free(size)) + +/* Scratch buffer for zero-malloc rendering - must match stb_truetype.h */ +#define STBTT_SCRATCH_DEFINED +struct stbtt_scratch { + char *buf; + size_t size; + size_t used; +}; + +static inline void stbtt_scratch_reset(struct stbtt_scratch *s) +{ + if (s) + s->used = 0; +} + +static inline void *stbtt__scratch_alloc(size_t size, void *userdata) +{ + struct stbtt_scratch *s = userdata; + size_t aligned = (size + 7) & ~7; + + if (s && s->used + aligned <= s->size) { + void *p = s->buf + s->used; + + s->used += aligned; + + return p; + } + + return malloc(size); +} + +static inline void stbtt__scratch_free(void *ptr, void *userdata) +{ + struct stbtt_scratch *s = userdata; + + if (!s || ptr < (void *)s->buf || ptr >= (void *)(s->buf + s->size)) + free(ptr); +} + +#define STBTT_malloc(size, u) stbtt__scratch_alloc(size, u) +#define STBTT_free(ptr, u) stbtt__scratch_free(ptr, u) #define STBTT_assert(x) #define STBTT_strlen(x) strlen(x) #define STBTT_memcpy memcpy @@ -184,6 +223,8 @@ struct console_tt_metrics { * this avoids malloc/free per character. Allocated lazily after * relocation to avoid using early malloc space. * @glyph_buf_size: Current size of glyph_buf in bytes + * @scratch: Scratch buffer state for stbtt internal allocations + * @scratch_buf: Memory for scratch buffer */ struct console_tt_priv { struct console_tt_metrics *cur_met; @@ -196,6 +237,8 @@ struct console_tt_priv { int pos_count; u8 *glyph_buf; int glyph_buf_size; + struct stbtt_scratch scratch; + char *scratch_buf; }; /** @@ -377,6 +420,9 @@ static int console_truetype_putc_xy(struct udevice *dev, uint x, uint y, if (priv->cur_fontdata) return console_fixed_putc_xy(dev, x, y, cp, priv->cur_fontdata); + /* Reset scratch buffer for this character */ + stbtt_scratch_reset(&priv->scratch); + /* First get some basic metrics about this character */ font = &met->font; stbtt_GetCodepointHMetrics(font, cp, &advance, &lsb); @@ -813,6 +859,7 @@ static int truetype_add_metrics(struct udevice *dev, const char *font_name, debug("%s: Font init failed\n", __func__); return -EPERM; } + font->userdata = &priv->scratch; /* Pre-calculate some things we will need regularly */ met->scale = stbtt_ScaleForPixelHeight(font, font_size); @@ -1217,6 +1264,17 @@ static int console_truetype_probe(struct udevice *dev) int ret; debug("%s: start\n", __func__); + + /* Allocate scratch buffer for stbtt internal allocations */ + if (CONFIG_IS_ENABLED(CONSOLE_TRUETYPE_SCRATCH)) { + priv->scratch_buf = malloc(CONFIG_CONSOLE_TRUETYPE_SCRATCH_SIZE); + if (priv->scratch_buf) { + priv->scratch.buf = priv->scratch_buf; + priv->scratch.size = CONFIG_CONSOLE_TRUETYPE_SCRATCH_SIZE; + priv->scratch.used = 0; + } + } + if (vid_priv->font_size) font_size = vid_priv->font_size; else diff --git a/drivers/video/stb_truetype.h b/drivers/video/stb_truetype.h index 90a5c2e2b3f..23a88898287 100644 --- a/drivers/video/stb_truetype.h +++ b/drivers/video/stb_truetype.h @@ -465,11 +465,53 @@ int main(int arg, char **argv) #define STBTT_fabs(x) fabs(x) #endif + /* Scratch buffer for zero-malloc rendering */ + #ifndef STBTT_SCRATCH_DEFINED + #define STBTT_SCRATCH_DEFINED + struct stbtt_scratch { + char *buf; + size_t size; + size_t used; + }; + + static inline void stbtt_scratch_reset(struct stbtt_scratch *s) + { + if (s) + s->used = 0; + } + #endif + // #define your own functions "STBTT_malloc" / "STBTT_free" to avoid malloc.h #ifndef STBTT_malloc #include <stdlib.h> - #define STBTT_malloc(x,u) ((void)(u),malloc(x)) - #define STBTT_free(x,u) ((void)(u),free(x)) + + static inline void *stbtt__scratch_alloc(size_t size, void *userdata) + { + struct stbtt_scratch *s = userdata; + size_t aligned = (size + 7) & ~7; /* 8-byte alignment */ + + if (s && s->used + aligned <= s->size) { + void *p = s->buf + s->used; + + s->used += aligned; + + return p; + } + + return malloc(size); /* fallback */ + } + + static inline void stbtt__scratch_free(void *ptr, void *userdata) + { + struct stbtt_scratch *s = userdata; + + /* Only free if not from scratch buffer */ + if (!s || ptr < (void *)s->buf || ptr >= (void *)(s->buf + s->size)) + free(ptr); + } + + #define STBTT_malloc(x,u) stbtt__scratch_alloc(x, u) + #define STBTT_free(x,u) stbtt__scratch_free(x, u) #endif #ifndef STBTT_assert -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> When copying partial framebuffer regions line by line, there is overhead from multiple memcpy() calls. Optimise video_flush_copy() to detect when entire lines are being copied (damage spans full width) and perform a single memcpy() for the whole region instead of looping line by line. Also invert the early-exit check to reduce nesting. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- drivers/video/video-uclass.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/drivers/video/video-uclass.c b/drivers/video/video-uclass.c index 3e02c48d399..698fe0e4caf 100644 --- a/drivers/video/video-uclass.c +++ b/drivers/video/video-uclass.c @@ -507,16 +507,25 @@ static void video_flush_copy(struct udevice *vid) if (!priv->copy_fb) return; - if (damage->x1 && damage->y1) { - int lstart = damage->x0 * VNBYTES(priv->bpix); - int lend = damage->x1 * VNBYTES(priv->bpix); + if (!damage->x1 || !damage->y1) + return; + + int lstart = damage->x0 * VNBYTES(priv->bpix); + int llen = damage->x1 * VNBYTES(priv->bpix) - lstart; + + /* Copy entire region at once if full lines are damaged */ + if (!lstart && llen == priv->line_length) { + ulong offset = damage->y0 * priv->line_length; + ulong len = (damage->y1 - damage->y0) * priv->line_length; + + memcpy(priv->copy_fb + offset, priv->fb + offset, len); + } else { int y; for (y = damage->y0; y < damage->y1; y++) { - ulong offset = (y * priv->line_length) + lstart; - ulong len = lend - lstart; + ulong offset = y * priv->line_length + lstart; - memcpy(priv->copy_fb + offset, priv->fb + offset, len); + memcpy(priv->copy_fb + offset, priv->fb + offset, llen); } } } -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a _base suffix to this test so that it is easier to run it by itself with test.py without also getting dm_test_host_dup() Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/dm/host.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/dm/host.c b/test/dm/host.c index a126cc9b3a5..3abf573a54e 100644 --- a/test/dm/host.c +++ b/test/dm/host.c @@ -16,7 +16,7 @@ #include <test/ut.h> /* Basic test of host interface */ -static int dm_test_host(struct unit_test_state *uts) +static int dm_test_host_base(struct unit_test_state *uts) { static char label[] = "test"; struct udevice *dev, *part, *chk, *blk; @@ -72,7 +72,7 @@ static int dm_test_host(struct unit_test_state *uts) return 0; } -DM_TEST(dm_test_host, UTF_SCAN_FDT); +DM_TEST(dm_test_host_base, UTF_SCAN_FDT); /* reusing the same label should work */ static int dm_test_host_dup(struct unit_test_state *uts) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> In some cases longer function names mean that 48 characters is not enough to determine the call path. Increase the default to 80 to handle this. Signed-off-by: Simon Glass <simon.glass@canonical.com> --- configs/sandbox_defconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig index e55aef8e7f9..eae9c5f5bd3 100644 --- a/configs/sandbox_defconfig +++ b/configs/sandbox_defconfig @@ -13,6 +13,7 @@ CONFIG_SYS_MEMTEST_START=0x00100000 CONFIG_SYS_MEMTEST_END=0x00101000 CONFIG_ULIB=y CONFIG_MCHECK_HEAP_PROTECTION=y +CONFIG_MCHECK_CALLER_LEN=80 CONFIG_EXAMPLES=y CONFIG_EFI_SECURE_BOOT=y CONFIG_EFI_RT_VOLATILE_STORE=y -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> When mcheck is enabled, malloc_usable_size() returns incorrect results because mem2chunk() is called on the offset user pointer rather than the actual chunk. The pointer returned to the user is offset by the mcheck header, but malloc_usable_size() is unaware of this. Add a wrapper that returns the user-requested size stored in the mcheck header. This fixes test failures when CONFIG_MCHECK_CALLER_LEN is set to larger values. Also add a wrapper for the case where MALLOC_DEBUG is enabled without MCHECK_HEAP_PROTECTION, since MALLOC_DEBUG makes dlmalloc_usable_size_impl() static but no public dlmalloc_usable_size exists outside the mcheck block. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- common/dlmalloc.c | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/common/dlmalloc.c b/common/dlmalloc.c index 78efdf5fd9a..71f81aeaec7 100644 --- a/common/dlmalloc.c +++ b/common/dlmalloc.c @@ -648,6 +648,7 @@ static inline void MALLOC_COPY(void *dest, const void *src, size_t sz) { memcpy( #define dlrealloc_impl dlrealloc #define dlmemalign_impl dlmemalign #define dlcalloc_impl dlcalloc +#define dlmalloc_usable_size_impl dlmalloc_usable_size #endif static bool malloc_testing; /* enable test mode */ @@ -5943,13 +5944,15 @@ int dlmallopt(int param_number, int value) { return change_mparam(param_number, value); } -size_t dlmalloc_usable_size(const void* mem) { - if (mem != 0) { - mchunkptr p = mem2chunk((void*)mem); - if (is_inuse(p)) - return chunksize(p) - overhead_for(p); - } - return 0; +STATIC_IF_MCHECK size_t dlmalloc_usable_size_impl(const void *mem) +{ + if (mem != 0) { + mchunkptr p = mem2chunk((void *)mem); + + if (is_inuse(p)) + return chunksize(p) - overhead_for(p); + } + return 0; } #if CONFIG_IS_ENABLED(MCHECK_HEAP_PROTECTION) @@ -6098,6 +6101,20 @@ void *dlcalloc(size_t n, size_t elem_size) return mcheck_alloc_noclean_posthook(p, n * elem_size, mcheck_caller()); } +size_t dlmalloc_usable_size(const void *mem) +{ + if (!mem) + return 0; + + if (mcheck_disabled) + return dlmalloc_usable_size_impl(mem); + + /* Return the user-requested size from mcheck header */ + const struct mcheck_hdr *hdr = &((const struct mcheck_hdr *)mem)[-1]; + + return hdr->size; +} + /* mcheck API */ int mcheck_pedantic(mcheck_abortfunc_t f) { @@ -6145,6 +6162,14 @@ void *dlcalloc(size_t n, size_t elem_size) } #endif /* MCHECK_HEAP_PROTECTION */ +#if CONFIG_IS_ENABLED(MALLOC_DEBUG) && !CONFIG_IS_ENABLED(MCHECK_HEAP_PROTECTION) +/* Wrapper needed when MALLOC_DEBUG makes dlmalloc_usable_size_impl static */ +size_t dlmalloc_usable_size(const void *mem) +{ + return dlmalloc_usable_size_impl(mem); +} +#endif + #if !CONFIG_IS_ENABLED(MCHECK_HEAP_PROTECTION) /* Stub when mcheck is not enabled */ void mcheck_set_disabled(bool disabled) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The malloc_very_large() test fails when mcheck is enabled with large CONFIG_MCHECK_CALLER_LEN because the 64K margin does not account for the per-allocation overhead (header + canaries). Use a larger margin (256K) when mcheck is enabled to ensure the test passes regardless of the mcheck caller length setting. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/common/malloc.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/common/malloc.c b/test/common/malloc.c index 9fdc1789645..436aac503be 100644 --- a/test/common/malloc.c +++ b/test/common/malloc.c @@ -535,11 +535,21 @@ COMMON_TEST(common_test_mallinfo, 0); /* Test allocating a very large size */ static int common_test_malloc_very_large(struct unit_test_state *uts) { - size_t size, before; + size_t size, before, margin; void *ptr; before = get_alloced_size(); - size = TOTAL_MALLOC_LEN - before - SZ_64K; + + /* + * When mcheck is enabled, it adds overhead per allocation (header + + * canaries). With large CONFIG_MCHECK_CALLER_LEN, this can be + * significant. Use a larger margin to account for mcheck overhead. + */ + if (CONFIG_IS_ENABLED(MCHECK_HEAP_PROTECTION)) + margin = SZ_256K; + else + margin = SZ_64K; + size = TOTAL_MALLOC_LEN - before - margin; ptr = malloc(size); ut_assertnonnull(ptr); -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Replace direct printf calls in malloc_dump() with an output callback function. This introduces dump_out_fn type and dump_to_console() helper, with malloc_dump_impl() taking the callback and context pointer. This allows the same implementation logic to be reused for different output destinations such as writing to a file. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- common/dlmalloc.c | 69 ++++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/common/dlmalloc.c b/common/dlmalloc.c index 71f81aeaec7..63792104ee3 100644 --- a/common/dlmalloc.c +++ b/common/dlmalloc.c @@ -7215,7 +7215,21 @@ static struct mcheck_hdr *find_freed_mcheck_hdr(void *mem, size_t sz) } #endif -void malloc_dump(void) +/* Output function type for malloc_dump_impl */ +typedef void (*dump_out_fn)(void *ctx, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); + +/* Console output function for heap dump */ +static void dump_to_console(void *ctx, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); +} + +static void malloc_dump_impl(dump_out_fn out, void *ctx) { mchunkptr q; msegmentptr s; @@ -7223,21 +7237,21 @@ void malloc_dump(void) int used_count = 0, free_count = 0; if (!is_initialized(gm)) { - printf("dlmalloc not initialized\n"); + out(ctx, "dlmalloc not initialized\n"); return; } - printf("Heap dump: %lx - %lx\n", mem_malloc_start, mem_malloc_end); - printf("%12s %10s %s\n", "Address", "Size", "Status"); - printf("----------------------------------\n"); + out(ctx, "Heap dump: %lx - %lx\n", mem_malloc_start, mem_malloc_end); + out(ctx, "%12s %10s %s\n", "Address", "Size", "Status"); + out(ctx, "----------------------------------\n"); s = &gm->seg; while (s != 0) { q = align_as_chunk(s->base); /* Show chunk header before first allocation */ - printf("%12lx %10zx (chunk header)\n", (ulong)s->base, - (size_t)((char *)chunk2mem(q) - (char *)s->base)); + out(ctx, "%12lx %10zx (chunk header)\n", (ulong)s->base, + (size_t)((char *)chunk2mem(q) - (char *)s->base)); while (segment_holds(s, q) && q != gm->top && q->head != FENCEPOST_HEAD) { @@ -7250,14 +7264,14 @@ void malloc_dump(void) hdr = find_mcheck_hdr_in_chunk(mem, sz); if (hdr && hdr->caller[0]) - printf("%12lx %10zx %s\n", - (ulong)mem, sz, hdr->caller); + out(ctx, "%12lx %10zx %s\n", + (ulong)mem, sz, hdr->caller); else - printf("%12lx %10zx\n", - (ulong)mem, sz); + out(ctx, "%12lx %10zx\n", + (ulong)mem, sz); #else - printf("%12lx %10zx\n", - (ulong)mem, sz); + out(ctx, "%12lx %10zx\n", + (ulong)mem, sz); #endif used += sz; used_count++; @@ -7267,14 +7281,14 @@ void malloc_dump(void) hdr = find_freed_mcheck_hdr(mem, sz); if (hdr && hdr->caller[0]) - printf("%12lx %10zx free %s\n", - (ulong)mem, sz, hdr->caller); + out(ctx, "%12lx %10zx free %s\n", + (ulong)mem, sz, hdr->caller); else - printf("%12lx %10zx free\n", - (ulong)mem, sz); + out(ctx, "%12lx %10zx free\n", + (ulong)mem, sz); #else - printf("%12lx %10zx free\n", - (ulong)mem, sz); + out(ctx, "%12lx %10zx free\n", + (ulong)mem, sz); #endif free_space += sz; free_count++; @@ -7286,15 +7300,20 @@ void malloc_dump(void) /* Print top chunk (wilderness) */ if (gm->top && gm->topsize > 0) { - printf("%12lx %10zx top\n", - (ulong)chunk2mem(gm->top), gm->topsize); + out(ctx, "%12lx %10zx top\n", + (ulong)chunk2mem(gm->top), gm->topsize); free_space += gm->topsize; } - printf("%12lx %10s end\n", mem_malloc_end, ""); - printf("----------------------------------\n"); - printf("Used: %zx bytes in %d chunks\n", used, used_count); - printf("Free: %zx bytes in %d chunks + top\n", free_space, free_count); + out(ctx, "%12lx %10s end\n", mem_malloc_end, ""); + out(ctx, "----------------------------------\n"); + out(ctx, "Used: %zx bytes in %d chunks\n", used, used_count); + out(ctx, "Free: %zx bytes in %d chunks + top\n", free_space, free_count); +} + +void malloc_dump(void) +{ + malloc_dump_impl(dump_to_console, NULL); } int initf_malloc(void) -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a malloc()-traffic log that records all malloc()-related calls with their addresses, sizes, and caller information. This is useful for debugging allocation patterns and finding the source of allocations that lack caller info in heap dumps. Each entry stores: - Operation type (alloc/free/realloc/memalign) - Pointer address - Size (and old size for realloc) - Full caller backtrace string On sandbox, the log buffer is allocated from host memory using os_malloc(), so it does not affect U-Boot's heap. The size is controlled by CONFIG_MCHECK_LOG_SIZE (default 512K entries). If the log fills up, it wraps around (circular buffer) and a warning is shown when dumping to indicate how many entries were lost. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- Kconfig | 22 +++++ common/dlmalloc.c | 198 ++++++++++++++++++++++++++++++++++++++--- doc/develop/malloc.rst | 14 +++ include/malloc.h | 72 +++++++++++++++ test/common/malloc.c | 56 ++++++++++++ 5 files changed, 350 insertions(+), 12 deletions(-) diff --git a/Kconfig b/Kconfig index 3b89f2c48dd..d47077d43bc 100644 --- a/Kconfig +++ b/Kconfig @@ -366,6 +366,28 @@ config MCHECK_CALLER_LEN Larger values provide more context but increase memory overhead per allocation. +config MCHECK_LOG + bool "Enable malloc traffic log" + depends on MCHECK_HEAP_PROTECTION && SANDBOX + default y + help + Enable a log of all malloc()/free()/realloc() calls. This records + each call with its address, size, and caller backtrace. Useful for + debugging allocation patterns and finding the source of memory + leaks. The log uses host memory so it does not affect U-Boot's + heap. + +config MCHECK_LOG_SIZE + int "Number of entries in malloc traffic log" + depends on MCHECK_LOG + default 65536 + help + Sets the number of entries in the malloc traffic log. Each entry + records a malloc()/free()/realloc() call with address, size, and + caller info. The log uses host memory (os_malloc()) so it does not + affect U-Boot's heap. Use 'malloc log start' to begin logging and + 'malloc log' to dump the log. + config SPL_SYS_MALLOC_F bool "Enable malloc() pool in SPL" depends on SPL_FRAMEWORK && SYS_MALLOC_F && SPL diff --git a/common/dlmalloc.c b/common/dlmalloc.c index 63792104ee3..bf60e3004d4 100644 --- a/common/dlmalloc.c +++ b/common/dlmalloc.c @@ -5957,6 +5957,7 @@ STATIC_IF_MCHECK size_t dlmalloc_usable_size_impl(const void *mem) #if CONFIG_IS_ENABLED(MCHECK_HEAP_PROTECTION) #include <backtrace.h> +#include <os.h> #include "mcheck_core.inc.h" /* Guard against recursive backtrace calls during malloc */ @@ -5993,8 +5994,152 @@ static const char *mcheck_caller(void) return caller; } +#if CONFIG_IS_ENABLED(MCHECK_LOG) +/* Malloc traffic logging for debugging allocation patterns */ + +#if IS_ENABLED(CONFIG_SANDBOX) +#define MLOG_SIZE CONFIG_MCHECK_LOG_SIZE +#else +#define MLOG_SIZE 1024 +#endif + +/** + * struct mlog_hdr - Header for the malloc traffic log + * + * @buf: Array of log entries + * @size: Number of entries in @buf + * @head: Index of next entry to write + * @count: Total number of entries written (may exceed @size if wrapped) + * @enabled: true if logging is active + */ +struct mlog_hdr { + struct mlog_entry *buf; + uint size; + uint head; + uint count; + bool enabled; +}; + +static struct mlog_hdr mlog __section(".data"); + +static void mlog_record(enum mlog_type type, void *ptr, size_t size, + size_t old_size, const char *caller) +{ + struct mlog_entry *ent; + + if (!mlog.enabled || !mlog.buf) + return; + + ent = &mlog.buf[mlog.head]; + ent->type = type; + ent->ptr = ptr; + ent->size = size; + ent->old_size = old_size; + if (caller) + strlcpy(ent->caller, caller, MLOG_CALLER_LEN); + else + ent->caller[0] = '\0'; + mlog.head = (mlog.head + 1) % mlog.size; + mlog.count++; +} + +void malloc_log_start(void) +{ + if (!mlog.buf) { + if (IS_ENABLED(CONFIG_SANDBOX)) { + mlog.buf = os_malloc(MLOG_SIZE * + sizeof(struct mlog_entry)); + if (!mlog.buf) { + printf("Failed to allocate malloc log buffer\n"); + return; + } + mlog.size = MLOG_SIZE; + } else { + printf("Malloc log not available on this platform\n"); + return; + } + } + memset(mlog.buf, '\0', mlog.size * sizeof(struct mlog_entry)); + mlog.head = 0; + mlog.count = 0; + mlog.enabled = true; +} + +void malloc_log_stop(void) +{ + mlog.enabled = false; +} + +int malloc_log_info(struct mlog_info *info) +{ + if (!mlog.buf) + return -ENOENT; + + if (mlog.count > mlog.size) + info->entry_count = mlog.size; + else + info->entry_count = mlog.count; + info->max_entries = mlog.size; + info->total_count = mlog.count; + + return 0; +} + +int malloc_log_entry(uint idx, struct mlog_entry **entryp) +{ + uint start, count; + + if (!mlog.buf) + return -ENOENT; + + if (mlog.count > mlog.size) { + count = mlog.size; + start = mlog.head; + } else { + count = mlog.count; + start = 0; + } + + if (idx >= count) + return -ERANGE; + + *entryp = &mlog.buf[(start + idx) % mlog.size]; + + return 0; +} + +#else /* !MCHECK_LOG */ + +static inline void mlog_record(enum mlog_type type, void *ptr, size_t size, + size_t old_size, const char *caller) +{ +} + +void malloc_log_start(void) +{ +} + +void malloc_log_stop(void) +{ +} + +int malloc_log_info(struct mlog_info *info) +{ + return -ENOENT; +} + +int malloc_log_entry(uint idx, struct mlog_entry **entryp) +{ + return -ENOENT; +} + +#endif /* MCHECK_LOG */ + void *dlmalloc(size_t bytes) { + const char *caller; + void *p; + /* * Skip mcheck for simple malloc (pre-relocation). Simple malloc is a * bump allocator that can't free, so mcheck overhead is useless and @@ -6007,13 +6152,17 @@ void *dlmalloc(size_t bytes) if (mcheck_disabled) return dlmalloc_impl(bytes CALLER_NULL); + caller = mcheck_caller(); mcheck_pedantic_prehook(); size_t fullsz = mcheck_alloc_prehook(bytes); - void *p = dlmalloc_impl(fullsz CALLER_NULL); + p = dlmalloc_impl(fullsz CALLER_NULL); if (!p) return p; - return mcheck_alloc_posthook(p, bytes, mcheck_caller()); + p = mcheck_alloc_posthook(p, bytes, caller); + mlog_record(MLOG_ALLOC, p, bytes, 0, caller); + + return p; } void dlfree(void *mem) @@ -6027,11 +6176,16 @@ void dlfree(void *mem) dlfree_impl(mem); return; } + mlog_record(MLOG_FREE, mem, 0, 0, mcheck_caller()); dlfree_impl(mcheck_free_prehook(mem)); } void *dlrealloc(void *oldmem, size_t bytes) { + const char *caller; + size_t old_size; + void *p; + #ifdef REALLOC_ZERO_BYTES_FREES if (bytes == 0) { if (oldmem) @@ -6046,18 +6200,26 @@ void *dlrealloc(void *oldmem, size_t bytes) if (mcheck_disabled) return dlrealloc_impl(oldmem, bytes); + caller = mcheck_caller(); + old_size = dlmalloc_usable_size(oldmem); mcheck_pedantic_prehook(); - void *p = mcheck_reallocfree_prehook(oldmem); + p = mcheck_reallocfree_prehook(oldmem); size_t newsz = mcheck_alloc_prehook(bytes); p = dlrealloc_impl(p, newsz); if (!p) return p; - return mcheck_alloc_noclean_posthook(p, bytes, mcheck_caller()); + p = mcheck_alloc_noclean_posthook(p, bytes, caller); + mlog_record(MLOG_REALLOC, p, bytes, old_size, caller); + + return p; } void *dlmemalign(size_t alignment, size_t bytes) { + const char *caller; + void *p; + if (CONFIG_IS_ENABLED(SYS_MALLOC_F) && !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) return memalign_simple(alignment, bytes); @@ -6065,24 +6227,31 @@ void *dlmemalign(size_t alignment, size_t bytes) if (mcheck_disabled) return dlmemalign_impl(alignment, bytes); + caller = mcheck_caller(); mcheck_pedantic_prehook(); size_t fullsz = mcheck_memalign_prehook(alignment, bytes); - void *p = dlmemalign_impl(alignment, fullsz); + p = dlmemalign_impl(alignment, fullsz); if (!p) return p; - return mcheck_memalign_posthook(alignment, p, bytes, mcheck_caller()); + p = mcheck_memalign_posthook(alignment, p, bytes, caller); + mlog_record(MLOG_MEMALIGN, p, bytes, 0, caller); + + return p; } /* dlpvalloc, dlvalloc redirect to dlmemalign, so they need no wrapping */ void *dlcalloc(size_t n, size_t elem_size) { + const char *caller; + size_t sz; + void *p; + if (CONFIG_IS_ENABLED(SYS_MALLOC_F) && !(gd->flags & GD_FLG_FULL_MALLOC_INIT)) { - size_t sz = n * elem_size; - void *p = malloc_simple(sz); - + sz = n * elem_size; + p = malloc_simple(sz); if (p) memset(p, '\0', sz); return p; @@ -6091,14 +6260,19 @@ void *dlcalloc(size_t n, size_t elem_size) if (mcheck_disabled) return dlcalloc_impl(n, elem_size); + sz = n * elem_size; + caller = mcheck_caller(); mcheck_pedantic_prehook(); /* NB: no overflow check here */ - size_t fullsz = mcheck_alloc_prehook(n * elem_size); - void *p = dlcalloc_impl(1, fullsz); + size_t fullsz = mcheck_alloc_prehook(sz); + p = dlcalloc_impl(1, fullsz); if (!p) return p; - return mcheck_alloc_noclean_posthook(p, n * elem_size, mcheck_caller()); + p = mcheck_alloc_noclean_posthook(p, sz, caller); + mlog_record(MLOG_ALLOC, p, sz, 0, caller); + + return p; } size_t dlmalloc_usable_size(const void *mem) diff --git a/doc/develop/malloc.rst b/doc/develop/malloc.rst index 8dba2d98afe..92180af055a 100644 --- a/doc/develop/malloc.rst +++ b/doc/develop/malloc.rst @@ -385,6 +385,20 @@ allocation was made:: This caller information makes it easy to track down memory leaks by showing exactly where each allocation originated. +Malloc-Traffic Log +~~~~~~~~~~~~~~~~~~ + +On sandbox, when mcheck is enabled, a malloc-traffic log can record all +malloc/free/realloc calls. This is useful for debugging allocation patterns +and finding where allocations without caller info originate. + +The log buffer is allocated from host memory using ``os_malloc()``, so it +does not affect U-Boot's heap. The size is controlled by +``CONFIG_MCHECK_LOG_SIZE`` (default 512K entries, about 80MB). + +If the log fills up, it wraps around and overwrites the oldest entries. +A warning is shown when dumping if entries were lost. + Valgrind ~~~~~~~~ diff --git a/include/malloc.h b/include/malloc.h index 3327bdcb44f..f8bfe843738 100644 --- a/include/malloc.h +++ b/include/malloc.h @@ -712,6 +712,78 @@ void malloc_disable_testing(void); */ void malloc_dump(void); +/** + * malloc_log_start() - Start logging malloc traffic + * + * Enables recording of malloc/free/realloc calls to a circular buffer. + * Use malloc_log_dump() to print the recorded entries. + */ +void malloc_log_start(void); + +/** + * malloc_log_stop() - Stop logging malloc traffic + */ +void malloc_log_stop(void); + +/** + * enum mlog_type - Type of malloc log entry + */ +enum mlog_type { + MLOG_ALLOC, + MLOG_FREE, + MLOG_REALLOC, + MLOG_MEMALIGN, +}; + +#define MLOG_CALLER_LEN 128 + +/** + * struct mlog_entry - Entry in the malloc traffic log + * + * @type: Operation type (alloc, free, realloc, memalign) + * @ptr: Pointer returned by or passed to the operation + * @size: Size of allocation (0 for free) + * @old_size: Previous size for realloc, 0 otherwise + * @caller: Backtrace string showing where the call originated + */ +struct mlog_entry { + enum mlog_type type; + void *ptr; + size_t size; + size_t old_size; + char caller[MLOG_CALLER_LEN]; +}; + +/** + * struct mlog_info - Information about the malloc log + * + * @entry_count: Number of entries currently available in the log + * @max_entries: Maximum number of entries the log can hold + * @total_count: Total number of entries recorded (may exceed max if wrapped) + */ +struct mlog_info { + uint entry_count; + uint max_entries; + uint total_count; +}; + +/** + * malloc_log_info() - Get information about the malloc log + * + * @info: Returns log statistics + * Return: 0 on success, -ENOENT if log not started + */ +int malloc_log_info(struct mlog_info *info); + +/** + * malloc_log_entry() - Get a specific entry from the malloc log + * + * @idx: Index of entry to retrieve (0 = oldest available) + * @entryp: Returns pointer to the log entry + * Return: 0 on success, -ENOENT if log not started, -ERANGE if idx out of range + */ +int malloc_log_entry(uint idx, struct mlog_entry **entryp); + /** * malloc_backtrace_skip() - Control backtrace collection in malloc * diff --git a/test/common/malloc.c b/test/common/malloc.c index 436aac503be..4e5bd68c013 100644 --- a/test/common/malloc.c +++ b/test/common/malloc.c @@ -637,3 +637,59 @@ static int common_test_malloc_fill_pool(struct unit_test_state *uts) return 0; } COMMON_TEST(common_test_malloc_fill_pool, 0); + +#if CONFIG_IS_ENABLED(MCHECK_LOG) +/* Test malloc_log_info() and malloc_log_entry() */ +static int common_test_malloc_log_info(struct unit_test_state *uts) +{ + struct mlog_entry *entry; + struct mlog_info info; + void *ptr, *ptr2; + + /* Should fail before log is started */ + ut_asserteq(-ENOENT, malloc_log_info(&info)); + ut_asserteq(-ENOENT, malloc_log_entry(0, &entry)); + + malloc_log_start(); + + /* Do an allocation, realloc, and free */ + ptr = malloc(0x100); + ut_assertnonnull(ptr); + + ptr2 = realloc(ptr, 0x200); + ut_assertnonnull(ptr2); + + free(ptr2); + + malloc_log_stop(); + + ut_assertok(malloc_log_info(&info)); + ut_asserteq(3, info.entry_count); + ut_assert(info.max_entries > 0); + ut_asserteq(info.entry_count, info.total_count); /* no wrapping */ + + /* Check the alloc entry */ + ut_assertok(malloc_log_entry(0, &entry)); + ut_asserteq(MLOG_ALLOC, entry->type); + ut_asserteq_ptr(ptr, entry->ptr); + ut_asserteq(0x100, entry->size); + + /* Check the realloc entry */ + ut_assertok(malloc_log_entry(1, &entry)); + ut_asserteq(MLOG_REALLOC, entry->type); + ut_asserteq_ptr(ptr2, entry->ptr); + ut_asserteq(0x200, entry->size); + ut_asserteq(0x100, entry->old_size); + + /* Check the free entry */ + ut_assertok(malloc_log_entry(2, &entry)); + ut_asserteq(MLOG_FREE, entry->type); + ut_asserteq_ptr(ptr2, entry->ptr); + + /* Out of range should fail */ + ut_asserteq(-ERANGE, malloc_log_entry(3, &entry)); + + return 0; +} +COMMON_TEST(common_test_malloc_log_info, 0); +#endif /* MCHECK_LOG */ -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add malloc_log_dump() to print all recorded malloc/free/realloc calls with their addresses, sizes, and caller information. This provides a way to inspect the log after recording. The dump shows a summary line with entry counts, followed by a table with sequence number, operation type, pointer, size, and caller for each recorded operation. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- common/dlmalloc.c | 70 ++++++++++++++++++++++++++++++++++++++++++++ include/malloc.h | 8 +++++ test/common/malloc.c | 41 ++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/common/dlmalloc.c b/common/dlmalloc.c index bf60e3004d4..6f10a980d6e 100644 --- a/common/dlmalloc.c +++ b/common/dlmalloc.c @@ -6070,6 +6070,72 @@ void malloc_log_stop(void) mlog.enabled = false; } +/* Output function type for malloc_log_impl */ +typedef void (*log_out_fn)(void *ctx, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); + +/* Console output function for log */ +static void log_to_console(void *ctx, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + vprintf(fmt, args); + va_end(args); +} + +static void malloc_log_impl(log_out_fn out, void *ctx) +{ + static const char * const mlog_type_names[] = { + "alloc", "free", "realloc", "memalign" + }; + uint i, start, count; + + if (!mlog.buf) { + out(ctx, "Malloc log not started\n"); + return; + } + + if (mlog.enabled) { + out(ctx, "Warning: log still active, results may be incomplete\n"); + malloc_log_stop(); + } + + if (mlog.count > mlog.size) { + out(ctx, "Warning: log wrapped, %u entries lost\n", + mlog.count - mlog.size); + count = mlog.size; + start = mlog.head; /* oldest entry is at current head */ + } else { + count = mlog.count; + start = 0; + } + + out(ctx, "Malloc log: %u entries (max %u, total %u)\n", count, mlog.size, + mlog.count); + out(ctx, "%4s %-8s %10s %8s %s\n", "Seq", "Type", "Address", "Size", + "Caller"); + out(ctx, "---- -------- ---------- -------- ------\n"); + + for (i = 0; i < count; i++) { + uint idx = (start + i) % mlog.size; + struct mlog_entry *ent = &mlog.buf[idx]; + + out(ctx, "%4u %-8s %10lx %8lx", i, mlog_type_names[ent->type], + (ulong)map_to_sysmem(ent->ptr), (ulong)ent->size); + if (ent->type == MLOG_REALLOC) + out(ctx, " (was %lx)", (ulong)ent->old_size); + if (ent->caller[0]) + out(ctx, " %s", ent->caller); + out(ctx, "\n"); + } +} + +void malloc_log_dump(void) +{ + malloc_log_impl(log_to_console, NULL); +} + int malloc_log_info(struct mlog_info *info) { if (!mlog.buf) @@ -6123,6 +6189,10 @@ void malloc_log_stop(void) { } +void malloc_log_dump(void) +{ +} + int malloc_log_info(struct mlog_info *info) { return -ENOENT; diff --git a/include/malloc.h b/include/malloc.h index f8bfe843738..4bd6458b70d 100644 --- a/include/malloc.h +++ b/include/malloc.h @@ -725,6 +725,14 @@ void malloc_log_start(void); */ void malloc_log_stop(void); +/** + * malloc_log_dump() - Dump the malloc traffic log + * + * Prints all recorded malloc/free/realloc calls with their addresses, + * sizes, and caller information. + */ +void malloc_log_dump(void); + /** * enum mlog_type - Type of malloc log entry */ diff --git a/test/common/malloc.c b/test/common/malloc.c index 4e5bd68c013..d29e9364b01 100644 --- a/test/common/malloc.c +++ b/test/common/malloc.c @@ -692,4 +692,45 @@ static int common_test_malloc_log_info(struct unit_test_state *uts) return 0; } COMMON_TEST(common_test_malloc_log_info, 0); + +/* Test malloc_log_dump() */ +static int common_test_malloc_log_dump(struct unit_test_state *uts) +{ + struct mlog_info info; + void *ptr, *ptr2; + + malloc_log_start(); + + /* Do an allocation, realloc, and free */ + ptr = malloc(0x100); + ut_assertnonnull(ptr); + + ptr2 = realloc(ptr, 0x200); + ut_assertnonnull(ptr2); + + free(ptr2); + + malloc_log_stop(); + + ut_assertok(malloc_log_info(&info)); + + console_record_reset_enable(); + malloc_log_dump(); + + ut_assert_nextline("Malloc log: 3 entries (max %u, total 3)", + info.max_entries); + ut_assert_nextline("%4s %-8s %10s %8s %s", "Seq", "Type", "Address", + "Size", "Caller"); + ut_assert_nextline("---- -------- ---------- -------- ------"); + ut_assert_nextlinen(" 0 alloc %10lx 100", + (ulong)map_to_sysmem(ptr)); + ut_assert_nextlinen(" 1 realloc %10lx 200 (was 100)", + (ulong)map_to_sysmem(ptr2)); + ut_assert_nextlinen(" 2 free %10lx 0", + (ulong)map_to_sysmem(ptr2)); + ut_assert_console_end(); + + return 0; +} +COMMON_TEST(common_test_malloc_log_dump, 0); #endif /* MCHECK_LOG */ -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a command interface for the malloc-traffic log: - malloc log start: Start recording allocations - malloc log stop: Stop recording - malloc log: Dump the recorded entries Example output: => malloc log Malloc log: 29 entries (max 524288, total 29) Seq Type Ptr Size Caller ---- -------- ---------------- -------- ------ 0 free 16a016e0 0 free_pipe_list:2001 <-parse_stream_outer:3208 <-parse_file_outer:3300 1 alloc 16a01b90 20 hush_file_init:3277 <-parse_file_outer:3295 <-run_pipe_real:1986 Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- cmd/Kconfig | 10 +++++++++ cmd/malloc.c | 41 ++++++++++++++++++++++++++++++++-- doc/develop/malloc.rst | 3 ++- doc/usage/cmd/malloc.rst | 30 ++++++++++++++++++++++++- test/cmd/malloc.c | 48 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+), 4 deletions(-) diff --git a/cmd/Kconfig b/cmd/Kconfig index d602f430ab6..072ff879cd8 100644 --- a/cmd/Kconfig +++ b/cmd/Kconfig @@ -3049,6 +3049,16 @@ config CMD_MALLOC about memory allocation, such as total memory allocated and currently in use. +config CMD_MALLOC_LOG + bool "malloc log - Log malloc traffic" + depends on CMD_MALLOC && MCHECK_LOG + default y + help + This adds the 'malloc log' subcommand which records all + malloc/free/realloc calls with their addresses, sizes, and caller + information. Use 'malloc log start' to begin recording and + 'malloc log' to display the recorded entries. + config CMD_MOUSE bool "mouse - Show mouse input" default y if MOUSE diff --git a/cmd/malloc.c b/cmd/malloc.c index 9c7dfbfc0c3..361750c45dd 100644 --- a/cmd/malloc.c +++ b/cmd/malloc.c @@ -38,10 +38,47 @@ static int do_malloc_dump(struct cmd_tbl *cmdtp, int flag, int argc, return 0; } +static int __maybe_unused do_malloc_log(struct cmd_tbl *cmdtp, int flag, + int argc, char *const argv[]) +{ + if (argc < 2) { + malloc_log_dump(); + return 0; + } + + if (!strcmp(argv[1], "start")) { + malloc_log_start(); + printf("Malloc logging started\n"); + } else if (!strcmp(argv[1], "stop")) { + malloc_log_stop(); + printf("Malloc logging stopped\n"); + } else if (!strcmp(argv[1], "dump")) { + malloc_log_dump(); + } else { + return CMD_RET_USAGE; + } + + return 0; +} + +#if CONFIG_IS_ENABLED(CMD_MALLOC_LOG) +#define MALLOC_LOG_HELP \ + "malloc log [start|stop|dump] - log malloc traffic\n" \ + " start - start recording malloc/free calls\n" \ + " stop - stop recording\n" \ + " dump - print the log (or just 'malloc log')\n" +#define MALLOC_LOG_SUBCMD , U_BOOT_SUBCMD_MKENT(log, 3, 1, do_malloc_log) +#else +#define MALLOC_LOG_HELP +#define MALLOC_LOG_SUBCMD +#endif + U_BOOT_LONGHELP(malloc, "info - display malloc statistics\n" - "malloc dump - dump heap chunks (address, size, status)\n"); + "malloc dump - dump heap chunks (address, size, status)\n" + MALLOC_LOG_HELP); U_BOOT_CMD_WITH_SUBCMDS(malloc, "malloc information", malloc_help_text, U_BOOT_SUBCMD_MKENT(info, 1, 1, do_malloc_info), - U_BOOT_SUBCMD_MKENT(dump, 1, 1, do_malloc_dump)); + U_BOOT_SUBCMD_MKENT(dump, 1, 1, do_malloc_dump) + MALLOC_LOG_SUBCMD); diff --git a/doc/develop/malloc.rst b/doc/develop/malloc.rst index 92180af055a..b9ab884d419 100644 --- a/doc/develop/malloc.rst +++ b/doc/develop/malloc.rst @@ -390,7 +390,8 @@ Malloc-Traffic Log On sandbox, when mcheck is enabled, a malloc-traffic log can record all malloc/free/realloc calls. This is useful for debugging allocation patterns -and finding where allocations without caller info originate. +and finding where allocations without caller info originate. See +:doc:`../usage/cmd/malloc` for usage. The log buffer is allocated from host memory using ``os_malloc()``, so it does not affect U-Boot's heap. The size is controlled by diff --git a/doc/usage/cmd/malloc.rst b/doc/usage/cmd/malloc.rst index 3693034b41e..03f0669658b 100644 --- a/doc/usage/cmd/malloc.rst +++ b/doc/usage/cmd/malloc.rst @@ -13,6 +13,7 @@ Synopsis malloc info malloc dump + malloc log [start|stop] Description ----------- @@ -31,6 +32,13 @@ dump for debugging memory allocation issues. When CONFIG_MCHECK_HEAP_PROTECTION is enabled, the caller string is also shown if available. +log + Controls the malloc traffic log. With no argument, dumps the recorded log + entries. Use ``start`` to begin recording malloc/free/realloc calls, and + ``stop`` to stop recording. Each entry shows the operation type, pointer + address, size, and caller backtrace. This is useful for tracking down + memory leaks or understanding allocation patterns. + The total heap size is set by ``CONFIG_SYS_MALLOC_LEN``. Example @@ -71,11 +79,31 @@ With CONFIG_MCHECK_HEAP_PROTECTION enabled, the caller backtrace is shown:: 18a3b840 90 used of_alias_scan:911 <-board_init_ ... +With CONFIG_CMD_MALLOC_LOG enabled, the log subcommand is available:: + + => malloc log start + Malloc logging started + => ... do some operations ... + => malloc log stop + Malloc logging stopped + => malloc log + Malloc log: 5 entries (max 524288, total 5) + Seq Type Ptr Size Caller + ---- -------- ---------------- -------- ------ + 0 alloc 16a01b90 20 hush_file_init:3277 + <-parse_file_outer:3295 <-run_pipe_real:1986 + 1 alloc 16a01bc0 100 xmalloc:107 <-xzalloc:117 + <-new_pipe:1498 <-run_list_real:1702 + 2 free 16a01bc0 0 free_pipe_list:2001 + <-parse_stream_outer:3208 <-parse_file_outer:3300 + ... + Configuration ------------- The malloc command is enabled by CONFIG_CMD_MALLOC which depends on -CONFIG_MALLOC_DEBUG. +CONFIG_MALLOC_DEBUG. The log subcommand is enabled by CONFIG_CMD_MALLOC_LOG +which additionally requires CONFIG_MCHECK_LOG. Return value ------------ diff --git a/test/cmd/malloc.c b/test/cmd/malloc.c index 3c1a44bcacf..75e8afdec63 100644 --- a/test/cmd/malloc.c +++ b/test/cmd/malloc.c @@ -7,6 +7,7 @@ */ #include <malloc.h> +#include <mapmem.h> #include <dm/test.h> #include <test/cmd.h> #include <test/ut.h> @@ -52,3 +53,50 @@ static int cmd_test_malloc_dump(struct unit_test_state *uts) return 0; } CMD_TEST(cmd_test_malloc_dump, UTF_CONSOLE); + +#if CONFIG_IS_ENABLED(MCHECK_LOG) +/* Test 'malloc log' command */ +static int cmd_test_malloc_log(struct unit_test_state *uts) +{ + struct mlog_info info; + void *ptr, *ptr2; + int seq; + + ut_assertok(run_command("malloc log start", 0)); + ut_assert_nextline("Malloc logging started"); + ut_assert_console_end(); + + /* Get current log position so we know our sequence numbers */ + ut_assertok(malloc_log_info(&info)); + seq = info.total_count; + + /* Do allocations with distinctive sizes we can search for */ + ptr = malloc(12345); + ut_assertnonnull(ptr); + ptr2 = realloc(ptr, 23456); + ut_assertnonnull(ptr2); + free(ptr2); + + ut_assertok(run_command("malloc log stop", 0)); + ut_assert_nextline("Malloc logging stopped"); + ut_assert_console_end(); + + /* Dump the log and find our allocations by sequence number and size */ + ut_assertok(run_command("malloc log", 0)); + ut_assert_nextlinen("Malloc log: "); + ut_assert_nextline("%4s %-8s %10s %8s %s", + "Seq", "Type", "Address", "Size", "Caller"); + ut_assert_nextline("---- -------- ---------- -------- ------"); + /* 12345 = 0x3039, 23456 = 0x5ba0 */ + ut_assert_skip_to_linen("%4d alloc %10lx 3039", seq, + (ulong)map_to_sysmem(ptr)); + ut_assert_skip_to_linen("%4d realloc %10lx 5ba0", seq + 1, + (ulong)map_to_sysmem(ptr2)); + ut_assert_skip_to_linen("%4d free %10lx 0", seq + 2, + (ulong)map_to_sysmem(ptr2)); + console_record_reset_enable(); /* discard remaining output */ + + return 0; +} +CMD_TEST(cmd_test_malloc_log, UTF_CONSOLE); +#endif /* MCHECK_LOG */ -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Backtrace collection is relatively expensive and can significantly slow down malloc()-heavy code when mcheck is enabled. Add a new CONFIG_MCHECK_BACKTRACE option (default y) to allow disabling backtrace collection while keeping the other mcheck features (canaries, double-free detection, etc.) enabled. This allows using mcheck with less overhead when caller information is not needed. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- Kconfig | 10 ++++++++++ common/dlmalloc.c | 8 ++++++++ doc/develop/malloc.rst | 7 +++++++ 3 files changed, 25 insertions(+) diff --git a/Kconfig b/Kconfig index d47077d43bc..a058bd1a2aa 100644 --- a/Kconfig +++ b/Kconfig @@ -355,6 +355,16 @@ config MCHECK_HEAP_PROTECTION significantly increases memory overhead and should only be used for debugging. +config MCHECK_BACKTRACE + bool "Enable backtrace for mcheck caller info" + depends on MCHECK_HEAP_PROTECTION + default y + help + Enable collection of stack backtrace for each allocation. This + records the function/file/line that allocated the memory, useful + for debugging memory leaks. Disable this to reduce the overhead + of mcheck, since backtrace collection is relatively expensive. + config MCHECK_CALLER_LEN int "Length of caller string in mcheck header" depends on MCHECK_HEAP_PROTECTION diff --git a/common/dlmalloc.c b/common/dlmalloc.c index 6f10a980d6e..7f00dea6eb1 100644 --- a/common/dlmalloc.c +++ b/common/dlmalloc.c @@ -5960,6 +5960,7 @@ STATIC_IF_MCHECK size_t dlmalloc_usable_size_impl(const void *mem) #include <os.h> #include "mcheck_core.inc.h" +#if CONFIG_IS_ENABLED(MCHECK_BACKTRACE) /* Guard against recursive backtrace calls during malloc */ static bool in_backtrace __section(".data"); @@ -5968,6 +5969,7 @@ static bool in_backtrace __section(".data"); * Set via malloc_backtrace_skip() before calling panic(). */ static bool mcheck_skip_backtrace __section(".data"); +#endif /* Runtime flag to disable mcheck - allows bypassing heap protection */ static bool mcheck_disabled __section(".data"); @@ -5979,11 +5981,14 @@ void mcheck_set_disabled(bool disabled) void malloc_backtrace_skip(bool skip) { +#if CONFIG_IS_ENABLED(MCHECK_BACKTRACE) mcheck_skip_backtrace = skip; +#endif } static const char *mcheck_caller(void) { +#if CONFIG_IS_ENABLED(MCHECK_BACKTRACE) const char *caller = NULL; if (!in_backtrace && !mcheck_skip_backtrace) { @@ -5992,6 +5997,9 @@ static const char *mcheck_caller(void) in_backtrace = false; } return caller; +#else + return NULL; +#endif } #if CONFIG_IS_ENABLED(MCHECK_LOG) diff --git a/doc/develop/malloc.rst b/doc/develop/malloc.rst index b9ab884d419..776294809b8 100644 --- a/doc/develop/malloc.rst +++ b/doc/develop/malloc.rst @@ -130,6 +130,13 @@ Main U-Boot (post-relocation) ``malloc dump``. This significantly increases memory overhead and should only be used for debugging. Default: n +``CONFIG_MCHECK_BACKTRACE`` + Bool to enable backtrace collection for mcheck caller information. When + enabled (the default), each allocation records a stack backtrace showing + where it was made. This is useful for debugging memory leaks but adds + overhead to every malloc call. Disable this to reduce mcheck overhead + while keeping the canary checks and double-free detection. Default: y + xPL Boot Phases ~~~~~~~~~~~~~~~ -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add malloc_dump_to_file() and malloc_log_to_file() functions to write heap dumps and malloc traffic logs to host files. This is useful for debugging memory leaks in sandbox by allowing comparison of before/after heap states with external tools like diff. These functions are only available in sandbox builds since they use host-filesystem access. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- common/dlmalloc.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++ include/malloc.h | 22 +++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/common/dlmalloc.c b/common/dlmalloc.c index 7f00dea6eb1..e47f3c153a2 100644 --- a/common/dlmalloc.c +++ b/common/dlmalloc.c @@ -6144,6 +6144,38 @@ void malloc_log_dump(void) malloc_log_impl(log_to_console, NULL); } +#if IS_ENABLED(CONFIG_SANDBOX) +/* File output function for log */ +static void log_to_file(void *ctx, const char *fmt, ...) +{ + int fd = *(int *)ctx; + char buf[256]; + va_list args; + int len; + + va_start(args, fmt); + len = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + if (len > 0) + os_write(fd, buf, len); +} + +int malloc_log_to_file(const char *fname) +{ + int fd; + + fd = os_open(fname, OS_O_WRONLY | OS_O_CREAT | OS_O_TRUNC); + if (fd < 0) + return fd; + + malloc_log_impl(log_to_file, &fd); + os_close(fd); + + return 0; +} +#endif + int malloc_log_info(struct mlog_info *info) { if (!mlog.buf) @@ -7481,6 +7513,26 @@ static void dump_to_console(void *ctx, const char *fmt, ...) va_end(args); } +#if IS_ENABLED(CONFIG_SANDBOX) +#include <os.h> + +/* File output function for heap dump */ +static void dump_to_file(void *ctx, const char *fmt, ...) +{ + int fd = *(int *)ctx; + char buf[256]; + va_list args; + int len; + + va_start(args, fmt); + len = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + + if (len > 0) + os_write(fd, buf, len); +} +#endif + static void malloc_dump_impl(dump_out_fn out, void *ctx) { mchunkptr q; @@ -7568,6 +7620,22 @@ void malloc_dump(void) malloc_dump_impl(dump_to_console, NULL); } +#if IS_ENABLED(CONFIG_SANDBOX) +int malloc_dump_to_file(const char *fname) +{ + int fd; + + fd = os_open(fname, OS_O_WRONLY | OS_O_CREAT | OS_O_TRUNC); + if (fd < 0) + return fd; + + malloc_dump_impl(dump_to_file, &fd); + os_close(fd); + + return 0; +} +#endif + int initf_malloc(void) { #if CONFIG_IS_ENABLED(SYS_MALLOC_F) diff --git a/include/malloc.h b/include/malloc.h index 4bd6458b70d..3deb90b2a0b 100644 --- a/include/malloc.h +++ b/include/malloc.h @@ -712,6 +712,17 @@ void malloc_disable_testing(void); */ void malloc_dump(void); +/** + * malloc_dump_to_file() - Write heap dump to a host file + * + * @fname: Path to the output file on the host filesystem + * Return: 0 on success, negative error code on failure + * + * This is only available in sandbox builds. It writes the same information + * as malloc_dump() but to a file instead of the console. + */ +int malloc_dump_to_file(const char *fname); + /** * malloc_log_start() - Start logging malloc traffic * @@ -733,6 +744,17 @@ void malloc_log_stop(void); */ void malloc_log_dump(void); +/** + * malloc_log_to_file() - Write malloc()-traffic log to a host file + * + * @fname: Path to the output file on the host filesystem + * Return: 0 on success, negative error code on failure + * + * This is only available in sandbox builds. It writes the same information + * as malloc_log_dump() but to a file instead of the console. + */ +int malloc_log_to_file(const char *fname); + /** * enum mlog_type - Type of malloc log entry */ -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Document the practical workflow for detecting and diagnosing memory leaks in U-Boot, particularly in sandbox builds. This covers: - Using ut_check_delta() in unit tests for leak detection - Comparing heap dumps with malloc_dump_to_file() - Using malloc traffic logging to trace allocations - Verifying debug functions don't affect heap state - A step-by-step practical workflow Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- doc/develop/malloc.rst | 93 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/doc/develop/malloc.rst b/doc/develop/malloc.rst index 776294809b8..9becd573e8c 100644 --- a/doc/develop/malloc.rst +++ b/doc/develop/malloc.rst @@ -418,6 +418,99 @@ malloc testing Unit tests can use malloc_enable_testing() to simulate allocation failures. +Finding Memory Leaks +~~~~~~~~~~~~~~~~~~~~ + +U-Boot provides several tools for detecting and diagnosing memory leaks. +These techniques are primarily supported on sandbox, which has the full +debugging infrastructure enabled by default (``CONFIG_MALLOC_DEBUG``, +``CONFIG_MCHECK_HEAP_PROTECTION``) and access to host filesystem functions +for writing dump files. + +**Leak detection in unit tests** + +Unit tests can use ``ut_check_delta()`` to detect memory leaks:: + + ulong mem_start; + + mem_start = ut_check_delta(0); /* Record starting heap usage */ + + /* ... test code that allocates and frees memory ... */ + + ut_asserteq(0, ut_check_delta(mem_start)); /* Verify no leak */ + +This uses ``mallinfo().uordblks`` to compare heap usage before and after. + +**Heap dump comparison** + +When a leak is detected, use ``malloc_dump()`` to capture heap state before +and after the operation. In sandbox builds, ``malloc_dump_to_file()`` writes +the dump to a host file for easier comparison:: + + malloc_dump_to_file("/tmp/before.txt"); + + /* ... operation that may leak ... */ + + malloc_dump_to_file("/tmp/after.txt"); + +Then compare the dumps to find leaked allocations:: + + $ diff /tmp/before.txt /tmp/after.txt + +Or extract just the addresses and sizes for comparison:: + + $ awk '{print $1, $2}' /tmp/before.txt | sort > /tmp/b.txt + $ awk '{print $1, $2}' /tmp/after.txt | sort > /tmp/a.txt + $ comm -13 /tmp/b.txt /tmp/a.txt # Show allocations only in 'after' + +The dump includes caller information when ``CONFIG_MCHECK_HEAP_PROTECTION`` +is enabled, showing exactly where each leaked allocation originated. + +**Malloc-traffic logging** + +For more detailed analysis, use the malloc-traffic log to record all +allocations during an operation:: + + malloc_log_start(); + + /* ... operation to trace ... */ + + malloc_log_stop(); + malloc_log_to_file("/tmp/malloc_log.txt"); /* Sandbox only */ + +The log shows every malloc(), free(), and realloc() call with addresses, sizes, +and caller backtraces (if enabled). Search for allocations that were never +freed:: + + $ grep "alloc" /tmp/malloc_log.txt # Find all allocations + $ grep "16ad1290" /tmp/malloc_log.txt # Check if a specific address was freed + +**Verifying debug functions don't allocate** + +When using these debugging functions, verify they don't affect heap state +by checking ``malloc_get_info()`` before and after:: + + struct malloc_info before, after; + + malloc_get_info(&before); + malloc_dump_to_file("/tmp/dump.txt"); + malloc_get_info(&after); + + /* Verify no allocations occurred */ + assert(before.malloc_count == after.malloc_count); + assert(before.in_use_bytes == after.in_use_bytes); + +**Practical workflow** + +1. Add ``ut_check_delta()`` assertions to your test to detect leaks +2. When a leak is detected, add ``malloc_dump_to_file()`` calls before and + after the leaking operation +3. Run the test and compare the dump files to identify leaked allocations +4. Use the caller backtrace in the dump to find the allocation site +5. If more detail is needed, enable ``malloc_log_start()`` to trace all + allocations during the operation +6. Fix the leak and verify the test passes + API Reference ------------- -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> If the build does not exist we can get an error when starting up: INTERNALERROR> Exception: .config does not exist; try passing --build option? Exception ignored in atexit callback: <function cleanup at ...> Traceback (most recent call last): File "test/py/conftest.py", line 697, in cleanup show_timings() File "test/py/conftest.py", line 644, in show_timings if ubconfig and ubconfig.timing: ^^^^^^^^^^^^^^^ Fix this by setting up the timing member at the start. Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/py/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/py/conftest.py b/test/py/conftest.py index b79080af961..0060f3da986 100644 --- a/test/py/conftest.py +++ b/test/py/conftest.py @@ -301,6 +301,7 @@ def pytest_configure(config): ubconfig = ArbitraryAttributeContainer() ubconfig.brd = dict() ubconfig.env = dict() + ubconfig.timing = None not_found = [] with log.section('Loading lab modules', 'load_modules'): -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> In some cases the target sends a lot of data. This is typically limited to 4K on Linux (PTY size), but when (say) 100K of console output is sent, pytest uses all available CPU, often only reading 50 bytes at a time. Add a 1ms delay before polling, to help with this. Increase the read-buffer size from 1KB to 4KB to reduce the number of system calls. To test this change, dm_test_host() was modified to do 10 malloc_dump() calls, thus producing a lot of output. The impact of this patch is: total time 17s -> 6s total CPU 40s -> 18s Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/py/console_base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/py/console_base.py b/test/py/console_base.py index 86b6da1f5b0..6d117413aa3 100644 --- a/test/py/console_base.py +++ b/test/py/console_base.py @@ -32,6 +32,10 @@ pattern_lab_mode = re.compile('{lab mode.*}') TIMEOUT_MS = 30000 # Standard timeout TIMEOUT_CMD_MS = 10000 # Command-echo timeout +# Maximum bytes to read at once from the console. 4KB matches the typical +# Linux PTY buffer size (N_TTY_BUF_SIZE), so there's no benefit to larger. +RECV_BUF_SIZE = 4096 + # Timeout for board preparation in lab mode. This needs to be enough to build # U-Boot, write it to the board and then boot the board. Since this process is # under the control of another program (e.g. Labgrid), it will failure sooner @@ -803,7 +807,10 @@ class ConsoleBase(): events = self.p.poll.poll(poll_maxwait) if not events: raise Timeout() - c = self.p.receive(1024) + # Small delay to let more data accumulate in PTY buffer, to + # reduce CPU usage for test.py from 100% + time.sleep(0.001) + c = self.p.receive(RECV_BUF_SIZE) if self.logfile_read: self.logfile_read.write(c) self.buf += c -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> The VT100 escape sequence filter is applied to the entire buffer on every read iteration, causing O(n^2) performance. With 1.6MB of output from verbose unit tests and 1KB reads, this results in ~1600 iterations, each processing an ever-growing buffer. Fix this by filtering only the newly received data before appending it to the buffer. To test this, dm_test_host() was modified to do 10 malloc_dump() calls, thus producing a lot of output. The impact of this patch is: total time 6s -> 4.5s total CPU 18s -> 13s Note: Various other approaches were tried, including removing the concatenation to self.buf and updating expect() to only consider recent text. None of these yielded useful results. Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/py/console_base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/py/console_base.py b/test/py/console_base.py index 6d117413aa3..c4472420c31 100644 --- a/test/py/console_base.py +++ b/test/py/console_base.py @@ -813,11 +813,13 @@ class ConsoleBase(): c = self.p.receive(RECV_BUF_SIZE) if self.logfile_read: self.logfile_read.write(c) - self.buf += c + # Filter VT100 escapes from new data only, not entire buffer. + # This avoids O(n^2) behaviour when receiving large output. # count=0 is supposed to be the default, which indicates # unlimited substitutions, but in practice the version of # Python in Ubuntu 14.04 appears to default to count=2! - self.buf = self.re_vt100.sub('', self.buf, count=1000000) + c = self.re_vt100.sub('', c, count=1000000) + self.buf += c finally: if self.logfile_read: self.logfile_read.flush() -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> Add a --no-full option to pytest that passes -F to sandbox, skipping flat-tree tests. This allows running only live-tree tests for faster iteration during development. The default is to run full tests (both live and flat tree). Use --no-full to run only live-tree tests. Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/py/conftest.py | 6 ++++++ test/py/console_sandbox.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/test/py/conftest.py b/test/py/conftest.py index 0060f3da986..bd0c226276a 100644 --- a/test/py/conftest.py +++ b/test/py/conftest.py @@ -102,6 +102,10 @@ def pytest_addoption(parser): help='Show info on test timing') parser.addoption('-P', '--persist', default=False, action='store_true', help='Persist test artifacts (do not clean up after tests)') + parser.addoption('--no-timeout', default=False, action='store_true', + help='Disable console timeout (useful for debugging)') + parser.addoption('--no-full', default=False, action='store_true', + help='Skip flat-tree tests (run live-tree only)') def run_build(config, source_dir, build_dir, board_type, log): @@ -352,6 +356,8 @@ def pytest_configure(config): ubconfig.persist = config.getoption('persist') ubconfig.role = config.getoption('role') ubconfig.allow_exceptions = config.getoption('allow_exceptions') + ubconfig.no_timeout = config.getoption('no_timeout') + ubconfig.no_full = config.getoption('no_full') env_vars = ( 'board_type', diff --git a/test/py/console_sandbox.py b/test/py/console_sandbox.py index 565dce27e56..3bd109acef5 100644 --- a/test/py/console_sandbox.py +++ b/test/py/console_sandbox.py @@ -53,6 +53,10 @@ class ConsoleSandbox(ConsoleBase): cmd += ['-d', self.config.dtb] cmd += self.sandbox_flags + # Skip flat-tree tests if --no-full was passed + if self.config.no_full: + cmd.append('-F') + # Always disable the pager cmd.append('-P') -- 2.43.0
From: Simon Glass <simon.glass@canonical.com> When debugging, particularly when stepping through code in a debugger or dealing with very slow operations, the console timeout can interfere. Add a --no-timeout command-line option that disables the console timeout. Adjust get_default_timeout() to checks for both --gdbserver and --no-timeout, returning None to disable timeouts in either case. This consolidates the timeout-disable logic that was previously spread across multiple locations. Co-developed-by: Claude <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- test/py/console_base.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/test/py/console_base.py b/test/py/console_base.py index c4472420c31..7f044b587b6 100644 --- a/test/py/console_base.py +++ b/test/py/console_base.py @@ -271,6 +271,19 @@ class ConsoleBase(): # call, where the function returns None (assignment-from-none) return spawn.Spawn([]) + def get_default_timeout(self): + """Get the default timeout for commands. + + Subclasses can override this to provide a different timeout. + For example, sandbox may need a longer timeout when mcheck is enabled. + + Returns: + int: Timeout in milliseconds, or None if timeout is disabled + """ + if self.config.gdbserver or self.config.no_timeout: + return None + return TIMEOUT_MS + def eval_patterns(self): """Set up lists of regexes for patterns we don't expect on console""" self.bad_patterns = [pat.pattern for pat in self.avail_patterns @@ -328,7 +341,7 @@ class ConsoleBase(): m = pattern_ready_prompt.search(self.after) self.u_boot_version_string = m.group(2) self.log.info('Lab: Board is ready') - self.timeout = TIMEOUT_MS + self.timeout = self.get_default_timeout() break if m == 2: self.log.info(f'Found autoboot prompt {m}') @@ -616,8 +629,7 @@ class ConsoleBase(): if self.p: # Reset the console timeout value as some tests may change # its default value during the execution - if not self.config.gdbserver: - self.timeout = TIMEOUT_MS + self.timeout = self.get_default_timeout() return try: self.log.start_section('Starting U-Boot') @@ -628,8 +640,7 @@ class ConsoleBase(): # text if LCD is enabled. This value may need tweaking in the # future, possibly per-test to be optimal. This works for 'help' # on board 'seaboard'. - if not self.config.gdbserver: - self.timeout = TIMEOUT_MS + self.timeout = self.get_default_timeout() self.logfile_read = self.logstream if self.config.use_running_system: # Send an empty command to set up the 'expect' logic. This has -- 2.43.0
participants (1)
-
Simon Glass