mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Namhyung Kim <namhyung@kernel.org>
To: haghdoost@uber.com
Cc: Peter Zijlstra <peterz@infradead.org>,
	Ingo Molnar <mingo@redhat.com>,
	Arnaldo Carvalho de Melo <acme@kernel.org>,
	Mark Rutland <mark.rutland@arm.com>,
	Alexander Shishkin <alexander.shishkin@linux.intel.com>,
	Jiri Olsa <jolsa@kernel.org>, Ian Rogers <irogers@google.com>,
	Adrian Hunter <adrian.hunter@intel.com>,
	James Clark <james.clark@linaro.org>,
	Andrii Nakryiko <andriin@fb.com>,
	Alexei Starovoitov <ast@kernel.org>,
	linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH 3/4] perf script: Add --lazy-load-symbols for lazy symbol loading
Date: Thu, 17 Sep 2026 00:24:24 -0700	[thread overview]
Message-ID: <aquVqBiZbVYceOJ2@z2> (raw)
In-Reply-To: <20260915-perf-symbol-memory-send-v1-3-1d3360e21f07@uber.com>

On Tue, Sep 15, 2026 at 11:42:45AM -0700, Alireza Haghdoost via B4 Relay wrote:
> From: Alireza Haghdoost <haghdoost@uber.com>
> 
> perf script eagerly materializes eligible symbols from every DSO
> encountered in samples. On a production fixture, it loaded about 765k
> symbols to resolve about 45k distinct (DSO, symbol) frames, exceeding the
> memory available in a memory-constrained cgroup.
> 
> This patch adds --lazy-load-symbols for userspace ELF DSOs. It builds a
> compact sorted index, resolves sampled addresses by binary search, reads
> symbol names with pread(), and caches resolved symbols in the existing
> rb-tree. Lazy lookup retains one CLOEXEC file descriptor per indexed DSO.
> If the descriptor cannot be retained, perf discards the index and eagerly
> loads that DSO instead.
> 
> On the same fixture, peak RssAnon drops from 265 MiB to 39 MiB and wall
> time from 3.1 seconds to 1.85 seconds. Memory optimizations usually cost
> time; this one does not because lazy loading skips many unnecessary
> calloc() calls and demangling operations. Output was byte-identical on
> the tested x86-64 workloads and an aarch64 capture.

Interesting!

> 
> Lazy loading is most effective when samples reference only a small
> fraction of the available symbols, such as profiles spanning many large
> DSOs. It still builds an index proportional to the total symbol count.
> Eager loading remains available for dense symbol coverage or cases
> requiring its broader ELF and architecture support.
> 
> This does not claim full parity with the eager loader. Lazy loading
> supports the common userspace ELF symtab/dynsym case, with these known
> differences:
> 
>   - .gnu_debugdata merging and PPC64 .opd are not handled.
>   - SHT_NOBITS re-reading, IFUNC PLT naming, and exact-tie alias ordering
>     are unreachable or output-equivalent on x86-64.

I'm curious how it could handle map__find_symbol_by_name().

> 
> Signed-off-by: Alireza Haghdoost <haghdoost@uber.com>
> Assisted-by: Kimi:K3
> ---
>  tools/perf/builtin-script.c      |   4 +-
>  tools/perf/util/dso.c            |  10 +
>  tools/perf/util/dso.h            |  29 ++
>  tools/perf/util/map.c            |   9 +-
>  tools/perf/util/symbol-elf.c     | 560 ++++++++++++++++++++++++++++++++++++++-
>  tools/perf/util/symbol-minimal.c |  11 +
>  tools/perf/util/symbol.c         |  58 ++--
>  tools/perf/util/symbol.h         |   4 +
>  tools/perf/util/symbol_conf.h    |   1 +
>  9 files changed, 664 insertions(+), 22 deletions(-)
> 
> diff --git a/tools/perf/builtin-script.c b/tools/perf/builtin-script.c
> index 50fa6ca6455a..81e56378d3d2 100644
> --- a/tools/perf/builtin-script.c
> +++ b/tools/perf/builtin-script.c
> @@ -4037,7 +4037,7 @@ static int parse_callret_trace(const struct option *opt __maybe_unused,
>  }
>  
>  static int parse_max_symbol_bytes(const struct option *opt,
> -				  const char *str, int unset)
> +				const char *str, int unset)
>  {
>  	unsigned long *max_bytes = (unsigned long *)opt->value;
>  	static struct parse_tag size_tags[] = {
> @@ -4166,6 +4166,8 @@ int cmd_script(int argc, const char **argv)
>  	OPT_CALLBACK(0, "max-symbol-bytes", &symbol_conf.max_symbol_bytes,
>  		     "size", "Limit bytes for ELF struct symbol (e.g. 128M; 0=unlimited)",
>  		     parse_max_symbol_bytes),
> +	OPT_BOOLEAN(0, "lazy-load-symbols", &symbol_conf.lazy_load_symbols,
> +		    "Resolve symbols lazily instead of loading full symtabs"),

Please add documentation as well.


>  	OPT_BOOLEAN(0, "reltime", &reltime, "Show time stamps relative to start"),
>  	OPT_BOOLEAN(0, "deltatime", &deltatime, "Show time stamps relative to previous event"),
>  	OPT_BOOLEAN('I', "show-info", &show_full_info,
[SNIP]
> diff --git a/tools/perf/util/symbol-elf.c b/tools/perf/util/symbol-elf.c
> index 914e42d21f70..e4d77e46e883 100644
> --- a/tools/perf/util/symbol-elf.c
> +++ b/tools/perf/util/symbol-elf.c
> @@ -2,6 +2,7 @@
>  #include <fcntl.h>
>  #include <stdio.h>
>  #include <errno.h>
> +#include <stdint.h>
>  #include <stdlib.h>
>  #include <string.h>
>  #include <unistd.h>
> @@ -600,6 +601,32 @@ static int dso__synthesize_plt_got_symbols(struct dso *dso, Elf *elf,
>   * And always look at the original dso, not at debuginfo packages, that
>   * have the PLT data stripped out (shdr_rel_plt.sh_type == SHT_NOBITS).
>   */

It seems the above comment belongs to the original function.


> +static void dso__clip_ondemand_symbols_at(struct dso *dso, u64 addr)
> +{
> +	struct dso_ondemand *od = dso__ondemand(dso);
> +	struct sym_idx *sym;
> +	u32 lo = 0, hi, mid;
> +
> +	if (!od)
> +		return;
> +
> +	hi = od->nr_sorted;
> +	while (lo < hi) {
> +		mid = (lo + hi) / 2;
> +		if (od->sorted[mid].start < addr)
> +			lo = mid + 1;
> +		else
> +			hi = mid;
> +	}

Why not use bsearch()?

> +
> +	if (!lo)
> +		return;
> +
> +	sym = &od->sorted[lo - 1];
> +	if (sym->end > addr)
> +		sym->end = addr;
> +}
> +
>  int dso__synthesize_plt_symbols(struct dso *dso, struct symsrc *ss)
>  {
>  	uint32_t idx;
> @@ -623,6 +650,8 @@ int dso__synthesize_plt_symbols(struct dso *dso, struct symsrc *ss)
>  	if (!elf_section_by_name(elf, &ehdr, &shdr_plt, ".plt", NULL))
>  		return 0;
>  
> +	dso__clip_ondemand_symbols_at(dso, shdr_plt.sh_offset);

Why is this needed?

> +
>  	/*
>  	 * A symbol from a previous section (e.g. .init) can have been expanded
>  	 * by symbols__fixup_end() to overlap .plt. Truncate it before adding
[SNIP]
> +static int dso__build_ondemand_index(struct dso *dso, struct symsrc *syms_ss,
> +				     struct symsrc *runtime_ss,
> +				     int dynsym)
> +{
> +	struct dso_ondemand *od;
> +	Elf *elf = syms_ss->elf;
> +	GElf_Ehdr ehdr = syms_ss->ehdr;
> +	GElf_Shdr shdr;
> +	GElf_Shdr strshdr;
> +	Elf_Scn *strscn, *sec_strndx;
> +	Elf_Data *syms;
> +	GElf_Sym sym;
> +	Elf_Data *secstrs = NULL;
> +	size_t i;
> +	u32 count = 0, j;
> +	u64 nr_entries, strtab_offset;
> +
> +	if (dynsym)
> +		shdr = syms_ss->dynshdr;
> +	else
> +		shdr = syms_ss->symshdr;
> +
> +	syms = elf_getdata(dynsym ? syms_ss->dynsym : syms_ss->symtab, NULL);
> +	if (!syms)
> +		return -1;
> +
> +	if (!shdr.sh_entsize)
> +		return 0;
> +
> +	nr_entries = shdr.sh_size / shdr.sh_entsize;
> +	if (nr_entries > UINT32_MAX)
> +		return -EOVERFLOW;
> +
> +	/*
> +	 * File offset of the string table linked from the symbol table.
> +	 * Symbol names are pread() from the file at lookup time, so we
> +	 * only need the offset and size here, not the strings themselves.
> +	 */
> +	strscn = elf_getscn(elf, shdr.sh_link);
> +	if (!strscn || !gelf_getshdr(strscn, &strshdr))
> +		return -1;
> +	strtab_offset = strshdr.sh_offset;
> +
> +	/*
> +	 * Section name string table, used to match the eager path's
> +	 * elf_sec__filter() (text/data section check for STT_NOTYPE labels).
> +	 */
> +	sec_strndx = elf_getscn(elf, ehdr.e_shstrndx);
> +	if (sec_strndx)
> +		secstrs = elf_getdata(sec_strndx, NULL);
> +
> +	/* Count symbols that pass the filter (same filter as fill below) */
> +	for (i = 0; i < nr_entries; i++) {
> +		if (!gelf_getsym(syms, i, &sym))
> +			continue;
> +		if (ondemand_sym_ok(elf, secstrs, &sym, shdr.sh_link,
> +				    ehdr.e_machine))
> +			count++;
> +	}
> +
> +	if (!count)
> +		return 0;
> +	if (count > SIZE_MAX / sizeof(*od->sorted))
> +		return -EOVERFLOW;
> +
> +	/*
> +	 * Account the index against the symbol memory budget: at 24
> +	 * bytes/symbol it is the dominant on-demand cost and must count
> +	 * toward --max-symbol-bytes just like struct symbol allocations do.
> +	 */
> +	if (symbol_conf.max_symbol_bytes &&
> +	    symbol__bytes_used() + count * sizeof(struct sym_idx) >
> +	    symbol_conf.max_symbol_bytes) {
> +		symbol_budget_warning();
> +		return 0; /* fall back to the eager loader's per-symbol budget */
> +	}
> +
> +	od = zalloc(sizeof(*od));
> +	if (!od)
> +		return -1;
> +
> +	od->sorted = malloc(count * sizeof(*od->sorted));
> +	if (!od->sorted) {
> +		free(od);
> +		return -1;
> +	}
> +	od->nr_alloc = count;	/* allocated; the deduped count may shrink */
> +
> +	/* Fill the index with adjusted addresses */
> +	j = 0;
> +	for (i = 0; i < nr_entries; i++) {
> +		u64 adjusted;
> +		GElf_Phdr phdr;
> +
> +		if (!gelf_getsym(syms, i, &sym))
> +			continue;
> +		if (!ondemand_sym_ok(elf, secstrs, &sym, shdr.sh_link,
> +				     ehdr.e_machine))
> +			continue;
> +
> +		adjusted = sym.st_value;
> +
> +		/* ARM Thumb bit fix (same as eager path, FUNC only) */
> +		if ((ehdr.e_machine == EM_ARM) &&
> +		    (GELF_ST_TYPE(sym.st_info) == STT_FUNC) &&
> +		    (adjusted & 1))
> +			--adjusted;
> +
> +		/*
> +		 * Program header adjustment, identical to the eager loop:
> +		 * read the PT_LOAD containing the symbol from the runtime
> +		 * ELF (the debug-info file may have zeroed p_offset), and
> +		 * fall back to the section-header bias when no program
> +		 * header matches -- exactly what the eager path does when
> +		 * elf_read_program_header fails.
> +		 */
> +		if (elf_read_program_header(runtime_ss->elf, adjusted,
> +					    &phdr) == 0) {
> +			adjusted -= phdr.p_vaddr - phdr.p_offset;
> +		} else {
> +			Elf_Scn *sym_sec = elf_getscn(elf, sym.st_shndx);
> +			GElf_Shdr sym_shdr;
> +
> +			if (sym_sec && gelf_getshdr(sym_sec, &sym_shdr))
> +				adjusted -= sym_shdr.sh_addr - sym_shdr.sh_offset;
> +		}
> +
> +		od->sorted[j].start = adjusted;
> +		od->sorted[j].end = sym.st_size; /* st_size for now, converted later */
> +		od->sorted[j].name_off = sym.st_name; /* strtab-relative */
> +		od->sorted[j].binding = GELF_ST_BIND(sym.st_info);
> +		od->sorted[j].type = GELF_ST_TYPE(sym.st_info);
> +		j++;
> +	}
> +
> +	/* Sort by adjusted start address */
> +	qsort(od->sorted, count, sizeof(*od->sorted), cmp_sym_idx);
> +
> +	/* Alias dedup: keep only the best symbol for each start address */
> +	if (!symbol_conf.allow_aliases) {

Probably better to factor out the dedup logic into a function.


> +		u32 out = 0;
> +
> +		for (i = 0; i < count; i++) {
> +			u32 best = i;
> +			const char *na = NULL, *nb;
> +			char *da = NULL, *db;
> +
> +			/* name_off is the strtab index (st_name) */
> +			na = elf_strptr(elf, shdr.sh_link,
> +					od->sorted[best].name_off);
> +			if (na) {
> +				da = dso__demangle_sym(dso, 0, na);
> +				if (da)
> +					na = da;
> +			}
> +
> +			/* Find the best among all entries with this start */
> +			for (j = i + 1; j < count &&
> +			     od->sorted[j].start == od->sorted[i].start; j++) {
> +				nb = elf_strptr(elf, shdr.sh_link,
> +						od->sorted[j].name_off);
> +				if (!na || !nb)
> +					continue;
> +
> +				/* Demangle for comparison, matching eager path */
> +				db = dso__demangle_sym(dso, 0, nb);
> +				if (db)
> +					nb = db;
> +
> +				/* od->sorted[].end holds st_size at this point */
> +				if (choose_best_symbol_raw(
> +					    od->sorted[best].end,
> +					    od->sorted[best].type,
> +					    od->sorted[best].binding, na,
> +					    od->sorted[j].end,
> +					    od->sorted[j].type,
> +					    od->sorted[j].binding, nb) == SYMBOL_B) {
> +					best = j;
> +					free(da);
> +					da = db;
> +					na = nb;
> +				} else {
> +					free(db);
> +				}
> +			}
> +
> +			free(da);
> +			od->sorted[out++] = od->sorted[best];
> +			i = j - 1; /* skip past all aliases of this start */
> +		}
> +
> +		if (out < count) {
> +			struct sym_idx *shrunk;
> +
> +			shrunk = realloc(od->sorted, out * sizeof(*od->sorted));
> +			if (shrunk) {
> +				od->sorted = shrunk;
> +				od->nr_alloc = out;
> +			}
> +		}
> +		count = out;
> +	}
> +
> +	/* Convert st_size to end addresses */
> +	for (i = 0; i < count; i++) {
> +		u64 size = od->sorted[i].end; /* was st_size */
> +
> +		if (size > 0)
> +			od->sorted[i].end = od->sorted[i].start + size;
> +		else if (i + 1 < count)
> +			od->sorted[i].end = od->sorted[i + 1].start;
> +		else
> +			/* Match symbols__fixup_end's last-symbol formula. */
> +			od->sorted[i].end = roundup(od->sorted[i].start, 4096) + 4096;
> +	}
> +
> +	/*
> +	 * Keep a private fd open for pread() of symbol names.  Dup with
> +	 * O_CLOEXEC so children don't inherit it, and so that
> +	 * symsrc__destroy() can close the original regardless of whether
> +	 * it is a real file or a temporary debugdata extraction.
> +	 *
> +	 * If the dup fails (e.g. fd exhaustion), decline by returning 0
> +	 * without setting the index: the caller falls back to the eager
> +	 * loader so the DSO still gets symbols rather than going symbol-less.
> +	 */
> +	od->fd = fcntl(syms_ss->fd, F_DUPFD_CLOEXEC, 0);
> +	if (od->fd < 0) {
> +		free(od->sorted);
> +		free(od);
> +		return 0;
> +	}
> +	od->strtab_offset = strtab_offset;
> +	od->strtab_size = strshdr.sh_size;
> +	od->nr_sorted = count;
> +
> +	symbol__account_bytes(od->nr_alloc * sizeof(*od->sorted));
> +
> +	dso__set_ondemand(dso, od);
> +
> +	pr_debug("%s: on-demand index: %u symbols\n",
> +		 dso__long_name(dso), count);
> +
> +	return 1;
> +}
> +
> +/*
> + * Read a NUL-terminated symbol name from the file's string table at
> + * file offset @off.  The fast path uses a stack buffer; if the NUL is
> + * not found within it (names can exceed 1 KiB for template-heavy C++
> + * mangled names), grow a heap buffer geometrically from 4 KiB, doubling
> + * until the terminator is found or the strtab is exhausted.  This keeps
> + * a single long name cheap even when it sits near the start of a large
> + * (tens of MB) strtab, while bounding a corrupt/missing terminator by
> + * the remaining strtab size.
> + *
> + * Returns a pointer to the name (either @buf or a heap allocation) and
> + * sets *@to_free to the buffer that must be free()d (NULL for @buf).
> + * Returns NULL on read error or if no NUL terminator exists within the
> + * strtab bounds.
> + */

We have dso-cache APIs to read file data (dso__data_read_offset) and it
manages file descriptors so you don't need to worry about FD exhaustion.


> +static const char *ondemand_read_name(int fd, u64 strtab_offset,
> +				      u64 strtab_size, u64 name_off,
> +				      char *buf, size_t buflen,
> +				      char **to_free)
> +{
> +	ssize_t n;
> +	u64 remain;
> +	u64 file_off;
> +	size_t cap;
> +
> +	*to_free = NULL;
> +
> +	/* name_off is strtab-relative (the symbol's st_name). */
> +	if (name_off >= strtab_size)
> +		return NULL;
> +	file_off = strtab_offset + name_off;
> +	remain = strtab_size - name_off;
> +
> +	/*
> +	 * Fast path: stack buffer, expect the name to fit.  Cap at the
> +	 * remaining strtab so a missing terminator can't read past the
> +	 * section into adjacent file data.
> +	 */
> +	n = pread(fd, buf, min((u64)(buflen - 1), remain), file_off);
> +	if (n <= 0)
> +		return NULL;
> +	buf[n] = '\0';
> +	if (memchr(buf, '\0', n))
> +		return buf;
> +
> +	/*
> +	 * Slow path: the name is longer than buflen.  Grow a heap buffer
> +	 * geometrically, doubling until the terminator appears, so a long
> +	 * name costs O(name length), not O(remaining strtab size).
> +	 */
> +	cap = 4096;
> +	for (;;) {
> +		char *tmp;
> +		size_t want = cap;
> +
> +		if (want > remain)
> +			want = remain;
> +		if (want == 0)
> +			break;
> +
> +		tmp = *to_free ? realloc(*to_free, want + 1) : malloc(want + 1);
> +		if (!tmp) {
> +			free(*to_free);
> +			*to_free = NULL;
> +			return NULL;
> +		}
> +		*to_free = tmp;
> +
> +		n = pread(fd, *to_free, want, file_off);
> +		if (n <= 0) {
> +			free(*to_free);
> +			*to_free = NULL;
> +			return NULL;
> +		}
> +		(*to_free)[n] = '\0';
> +
> +		if (memchr(*to_free, '\0', n))
> +			return *to_free;
> +
> +		/*
> +		 * Read the whole remaining strtab (or hit EOF) with no
> +		 * terminator: corrupt file, bail instead of re-reading.
> +		 */
> +		if (want >= remain || (u64)n >= remain)
> +			break;
> +
> +		/* Avoid size_t overflow on absurdly large strtabs. */
> +		if (cap > SIZE_MAX / 2)
> +			break;
> +		cap *= 2;
> +	}
> +
> +	free(*to_free);
> +	*to_free = NULL;
> +	return NULL;
> +}
> +
> +struct symbol *dso__find_symbol_ondemand(struct dso *dso, u64 addr)
> +{
> +	struct dso_ondemand *od = dso__ondemand(dso);
> +	u32 lo, hi, mid;
> +	const char *name;
> +	char namebuf[1024];
> +	char *name_heap = NULL;
> +	char *demangled;
> +	struct symbol *s = NULL;
> +
> +	if (!od || !od->sorted || od->fd < 0)
> +		return NULL;
> +
> +	lo = 0;
> +	hi = od->nr_sorted;
> +	while (lo < hi) {
> +		mid = (lo + hi) / 2;
> +		if (addr < od->sorted[mid].start)
> +			hi = mid;
> +		else if (addr >= od->sorted[mid].end)
> +			lo = mid + 1;
> +		else
> +			goto found;
> +	}

bsearch()?

Thanks,
Namhyung

> +
> +	/* Not found */
> +	return NULL;
> +
> +found:
> +	/*
> +	 * Check the budget before doing any name I/O or demangling, so an
> +	 * over-budget DSO stops paying pread+demangle on every later miss.
> +	 */
> +	if (symbol_conf.max_symbol_bytes &&
> +	    symbol__bytes_used() >= symbol_conf.max_symbol_bytes) {
> +		symbol_budget_warning();
> +		return NULL;
> +	}
> +
> +	name = ondemand_read_name(od->fd, od->strtab_offset, od->strtab_size,
> +				  od->sorted[mid].name_off,
> +				  namebuf, sizeof(namebuf), &name_heap);
> +	if (!name)
> +		return NULL;
> +
> +	demangled = dso__demangle_sym(dso, 0, name);
> +	if (demangled)
> +		name = demangled;
> +
> +	s = symbol__new(od->sorted[mid].start,
> +			od->sorted[mid].end - od->sorted[mid].start,
> +			od->sorted[mid].binding,
> +			od->sorted[mid].type, name);
> +	free(demangled);
> +	free(name_heap);
> +	if (s)
> +		__symbols__insert(dso__symbols(dso), s);
> +	return s;
> +}

  reply	other threads:[~2026-09-17  7:24 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-15 18:42 [PATCH 0/4] perf script: Bounded and " Alireza Haghdoost via B4 Relay
2026-09-15 18:42 ` [PATCH 1/4] perf symbols: Fix broken ELF_C_READ_MMAP fallback guard Alireza Haghdoost via B4 Relay
2026-09-15 18:42 ` [PATCH 2/4] perf script: Add --max-symbol-bytes to bound ELF symbol memory Alireza Haghdoost via B4 Relay
2026-09-17  6:53   ` Namhyung Kim
2026-09-15 18:42 ` [PATCH 3/4] perf script: Add --lazy-load-symbols for lazy symbol loading Alireza Haghdoost via B4 Relay
2026-09-17  7:24   ` Namhyung Kim [this message]
2026-09-15 18:42 ` [PATCH 4/4] perf script: Document and test --lazy-load-symbols and --max-symbol-bytes Alireza Haghdoost via B4 Relay

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=aquVqBiZbVYceOJ2@z2 \
    --to=namhyung@kernel.org \
    --cc=acme@kernel.org \
    --cc=adrian.hunter@intel.com \
    --cc=alexander.shishkin@linux.intel.com \
    --cc=andriin@fb.com \
    --cc=ast@kernel.org \
    --cc=haghdoost@uber.com \
    --cc=irogers@google.com \
    --cc=james.clark@linaro.org \
    --cc=jolsa@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-perf-users@vger.kernel.org \
    --cc=mark.rutland@arm.com \
    --cc=mingo@redhat.com \
    --cc=peterz@infradead.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®