* [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x
@ 2024-07-15 10:04 Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 1/5] debuginfod: factor out common code for responding from an archive Omar Sandoval
` (5 more replies)
0 siblings, 6 replies; 10+ messages in thread
From: Omar Sandoval @ 2024-07-15 10:04 UTC (permalink / raw)
To: elfutils-devel; +Cc: linux-debuggers
From: Omar Sandoval <osandov@fb.com>
This is v2 of my patch series optimizing debuginfod for kernel
debuginfo. v1 is here [7].
The main change from v1 is reworking the database changes to be backward
compatible and therefore not require reindexing.
Patch 1 is a preparatory refactor. Patch 2 makes the schema changes.
Patch 3 implements the seekable xz extraction. Patch 4 populates the
table of seekable entries at scan time. Patch 5 does it for
pre-existing files at request time.
Here is the background copied and pasted from v1:
drgn [1] currently uses debuginfod with great success for debugging
userspace processes. However, for debugging the Linux kernel (drgn's
main use case), we have had some performance issues with debuginfod, so
we intentionally avoid using it. Specifically, it sometimes takes over
a minute for debuginfod to respond to queries for vmlinux and kernel
modules (not including the actual download time).
The reason for the slowness is that Linux kernel debuginfo packages are
very large and contain lots of files. To respond to a query for a Linux
kernel debuginfo file, debuginfod has to decompress and iterate through
the whole package until it finds that file. If the file is towards the
end of the package, this can take a very long time. This was previously
reported for vdso files [2][3], which debuginfod was able to mitigate
with improved caching and prefetching. However, kernel modules are far
greater in number, vary drastically by hardware and workload, and can be
spread all over the package, so in practice I've still been seeing long
delays. This was also discussed on the drgn issue tracker [4].
The fundamental limitation is that Linux packages, which are essentially
compressed archives with extra metadata headers, don't support random
access to specific files. However, the multi-threaded xz compression
format does actually support random access. And, luckily, the kernel
debuginfo packages on Fedora, Debian, and Ubuntu all happen to use
multi-threaded xz compression!
debuginfod can take advantage of this: when it scans a package, if it is
a seekable xz archive, it can save the uncompressed offset and size of
each file. Then, when it needs a file, it can seek to that offset and
extract it from there. This requires some understanding of the xz
format and low-level liblzma code, but the speedup is massive: where the
worst case was previously about 50 seconds just to find a file in a
kernel debuginfo package, with this change the worst case is 0.25
seconds, a ~200x improvement! This works for both .rpm and .deb files.
I tested this by requesting and verifying the digest of every file from
a few kernel debuginfo rpms and debs [5].
P.S. The biggest downside of this change is that it depends on a very
specific compression format that is only used by kernel packages
incidentally. I think this is something we should formalize with Linux
distributions: large debuginfo packages should use a seekable format.
Currently, xz in multi-threaded mode is the only option, but Zstandard
also has an experimental seekable format that is worth looking into [6].
Thanks,
Omar
1: https://github.com/osandov/drgn
2: https://sourceware.org/bugzilla/show_bug.cgi?id=29478
3: https://bugzilla.redhat.com/show_bug.cgi?id=1970578
4: https://github.com/osandov/drgn/pull/380
5: https://gist.github.com/osandov/89d521fdc6c9a07aa8bb0ebf91974346
6: https://github.com/facebook/zstd/tree/dev/contrib/seekable_format
7: https://sourceware.org/pipermail/elfutils-devel/2024q3/007191.html
Omar Sandoval (5):
debuginfod: factor out common code for responding from an archive
debugifod: add new table and views for seekable archives
debuginfod: optimize extraction from seekable xz archives
debuginfod: populate _r_seekable on scan
debuginfod: populate _r_seekable on request
configure.ac | 5 +
debuginfod/Makefile.am | 2 +-
debuginfod/debuginfod.cxx | 921 ++++++++++++++++++++++++++++++++------
3 files changed, 788 insertions(+), 140 deletions(-)
--
2.45.2
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v2 1/5] debuginfod: factor out common code for responding from an archive
2024-07-15 10:04 [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Omar Sandoval
@ 2024-07-15 10:04 ` Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 2/5] debugifod: add new table and views for seekable archives Omar Sandoval
` (4 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Omar Sandoval @ 2024-07-15 10:04 UTC (permalink / raw)
To: elfutils-devel; +Cc: linux-debuggers
From: Omar Sandoval <osandov@fb.com>
handle_buildid_r_match has two very similar branches where it optionally
extracts a section and then creates a microhttpd response. In
preparation for adding a third one, factor it out into a function.
Signed-off-by: Omar Sandoval <osandov@fb.com>
---
debuginfod/debuginfod.cxx | 213 +++++++++++++++++---------------------
1 file changed, 96 insertions(+), 117 deletions(-)
diff --git a/debuginfod/debuginfod.cxx b/debuginfod/debuginfod.cxx
index 305edde8..2d709026 100644
--- a/debuginfod/debuginfod.cxx
+++ b/debuginfod/debuginfod.cxx
@@ -1965,6 +1965,81 @@ string canonicalized_archive_entry_pathname(struct archive_entry *e)
}
+// NB: takes ownership of, and may reassign, fd.
+static struct MHD_Response*
+create_buildid_r_response (int64_t b_mtime0,
+ const string& b_source0,
+ const string& b_source1,
+ const string& section,
+ const string& ima_sig,
+ const char* tmppath,
+ int& fd,
+ off_t size,
+ time_t mtime,
+ const string& metric,
+ const struct timespec& extract_begin)
+{
+ if (tmppath != NULL)
+ {
+ struct timespec extract_end;
+ clock_gettime (CLOCK_MONOTONIC, &extract_end);
+ double extract_time = (extract_end.tv_sec - extract_begin.tv_sec)
+ + (extract_end.tv_nsec - extract_begin.tv_nsec)/1.e9;
+ fdcache.intern(b_source0, b_source1, tmppath, size, true, extract_time);
+ }
+
+ if (!section.empty ())
+ {
+ int scn_fd = extract_section (fd, b_mtime0,
+ b_source0 + ":" + b_source1,
+ section, extract_begin);
+ close (fd);
+ if (scn_fd >= 0)
+ fd = scn_fd;
+ else
+ {
+ if (verbose)
+ obatched (clog) << "cannot find section " << section
+ << " for archive " << b_source0
+ << " file " << b_source1 << endl;
+ return 0;
+ }
+
+ struct stat fs;
+ if (fstat (fd, &fs) < 0)
+ {
+ close (fd);
+ throw libc_exception (errno,
+ string ("fstat ") + b_source0 + string (" ") + section);
+ }
+ size = fs.st_size;
+ }
+
+ struct MHD_Response* r = MHD_create_response_from_fd (size, fd);
+ if (r == 0)
+ {
+ if (verbose)
+ obatched(clog) << "cannot create fd-response for " << b_source0 << endl;
+ close(fd);
+ }
+ else
+ {
+ inc_metric ("http_responses_total","result",metric);
+ add_mhd_response_header (r, "Content-Type", "application/octet-stream");
+ add_mhd_response_header (r, "X-DEBUGINFOD-SIZE", to_string(size).c_str());
+ add_mhd_response_header (r, "X-DEBUGINFOD-ARCHIVE", b_source0.c_str());
+ add_mhd_response_header (r, "X-DEBUGINFOD-FILE", b_source1.c_str());
+ if(!ima_sig.empty()) add_mhd_response_header(r, "X-DEBUGINFOD-IMASIGNATURE", ima_sig.c_str());
+ add_mhd_last_modified (r, mtime);
+ if (verbose > 1)
+ obatched(clog) << "serving " << metric << " " << b_source0
+ << " file " << b_source1
+ << " section=" << section
+ << " IMA signature=" << ima_sig << endl;
+ /* libmicrohttpd will close fd. */
+ }
+ return r;
+}
static struct MHD_Response*
handle_buildid_r_match (bool internal_req_p,
@@ -2142,57 +2217,15 @@ handle_buildid_r_match (bool internal_req_p,
break; // branch out of if "loop", to try new libarchive fetch attempt
}
- if (!section.empty ())
- {
- int scn_fd = extract_section (fd, fs.st_mtime,
- b_source0 + ":" + b_source1,
- section, extract_begin);
- close (fd);
- if (scn_fd >= 0)
- fd = scn_fd;
- else
- {
- if (verbose)
- obatched (clog) << "cannot find section " << section
- << " for archive " << b_source0
- << " file " << b_source1 << endl;
- return 0;
- }
-
- rc = fstat(fd, &fs);
- if (rc < 0)
- {
- close (fd);
- throw libc_exception (errno,
- string ("fstat archive ") + b_source0 + string (" file ") + b_source1
- + string (" section ") + section);
- }
- }
-
- struct MHD_Response* r = MHD_create_response_from_fd (fs.st_size, fd);
+ struct MHD_Response* r = create_buildid_r_response (b_mtime, b_source0,
+ b_source1, section,
+ ima_sig, NULL, fd,
+ fs.st_size,
+ fs.st_mtime,
+ "archive fdcache",
+ extract_begin);
if (r == 0)
- {
- if (verbose)
- obatched(clog) << "cannot create fd-response for " << b_source0 << endl;
- close(fd);
- break; // branch out of if "loop", to try new libarchive fetch attempt
- }
-
- inc_metric ("http_responses_total","result","archive fdcache");
-
- add_mhd_response_header (r, "Content-Type", "application/octet-stream");
- add_mhd_response_header (r, "X-DEBUGINFOD-SIZE",
- to_string(fs.st_size).c_str());
- add_mhd_response_header (r, "X-DEBUGINFOD-ARCHIVE", b_source0.c_str());
- add_mhd_response_header (r, "X-DEBUGINFOD-FILE", b_source1.c_str());
- if(!ima_sig.empty()) add_mhd_response_header(r, "X-DEBUGINFOD-IMASIGNATURE", ima_sig.c_str());
- add_mhd_last_modified (r, fs.st_mtime);
- if (verbose > 1)
- obatched(clog) << "serving fdcache archive " << b_source0
- << " file " << b_source1
- << " section=" << section
- << " IMA signature=" << ima_sig << endl;
- /* libmicrohttpd will close it. */
+ break; // branch out of if "loop", to try new libarchive fetch attempt
if (result_fd)
*result_fd = fd;
return r;
@@ -2307,13 +2340,12 @@ handle_buildid_r_match (bool internal_req_p,
tvs[1].tv_nsec = archive_entry_mtime_nsec(e);
(void) futimens (fd, tvs); /* best effort */
- struct timespec extract_end;
- clock_gettime (CLOCK_MONOTONIC, &extract_end);
- double extract_time = (extract_end.tv_sec - extract_begin.tv_sec)
- + (extract_end.tv_nsec - extract_begin.tv_nsec)/1.e9;
-
if (r != 0) // stage 3
{
+ struct timespec extract_end;
+ clock_gettime (CLOCK_MONOTONIC, &extract_end);
+ double extract_time = (extract_end.tv_sec - extract_begin.tv_sec)
+ + (extract_end.tv_nsec - extract_begin.tv_nsec)/1.e9;
// NB: now we know we have a complete reusable file; make fdcache
// responsible for unlinking it later.
fdcache.intern(b_source0, fn,
@@ -2324,69 +2356,16 @@ handle_buildid_r_match (bool internal_req_p,
continue;
}
- // NB: now we know we have a complete reusable file; make fdcache
- // responsible for unlinking it later.
- fdcache.intern(b_source0, b_source1,
- tmppath, archive_entry_size(e),
- true, extract_time); // requested ones go to the front of the line
-
- if (!section.empty ())
- {
- int scn_fd = extract_section (fd, b_mtime,
- b_source0 + ":" + b_source1,
- section, extract_begin);
- close (fd);
- if (scn_fd >= 0)
- fd = scn_fd;
- else
- {
- if (verbose)
- obatched (clog) << "cannot find section " << section
- << " for archive " << b_source0
- << " file " << b_source1 << endl;
- return 0;
- }
-
- rc = fstat(fd, &fs);
- if (rc < 0)
- {
- close (fd);
- throw libc_exception (errno,
- string ("fstat ") + b_source0 + string (" ") + section);
- }
- r = MHD_create_response_from_fd (fs.st_size, fd);
- }
- else
- r = MHD_create_response_from_fd (archive_entry_size(e), fd);
-
- inc_metric ("http_responses_total","result",archive_extension + " archive");
+ r = create_buildid_r_response (b_mtime, b_source0, b_source1, section,
+ ima_sig, tmppath, fd,
+ archive_entry_size(e),
+ archive_entry_mtime(e),
+ archive_extension + " archive",
+ extract_begin);
if (r == 0)
- {
- if (verbose)
- obatched(clog) << "cannot create fd-response for " << b_source0 << endl;
- close(fd);
- break; // assume no chance of better luck around another iteration; no other copies of same file
- }
- else
- {
- add_mhd_response_header (r, "Content-Type",
- "application/octet-stream");
- add_mhd_response_header (r, "X-DEBUGINFOD-SIZE",
- to_string(archive_entry_size(e)).c_str());
- add_mhd_response_header (r, "X-DEBUGINFOD-ARCHIVE", b_source0.c_str());
- add_mhd_response_header (r, "X-DEBUGINFOD-FILE", b_source1.c_str());
- if(!ima_sig.empty()) add_mhd_response_header(r, "X-DEBUGINFOD-IMASIGNATURE", ima_sig.c_str());
- add_mhd_last_modified (r, archive_entry_mtime(e));
- if (verbose > 1)
- obatched(clog) << "serving archive " << b_source0
- << " file " << b_source1
- << " section=" << section
- << " IMA signature=" << ima_sig << endl;
- /* libmicrohttpd will close it. */
- if (result_fd)
- *result_fd = fd;
- continue;
- }
+ break; // assume no chance of better luck around another iteration; no other copies of same file
+ if (result_fd)
+ *result_fd = fd;
}
// XXX: rpm/file not found: delete this R entry?
--
2.45.2
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 2/5] debugifod: add new table and views for seekable archives
2024-07-15 10:04 [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 1/5] debuginfod: factor out common code for responding from an archive Omar Sandoval
@ 2024-07-15 10:04 ` Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 3/5] debuginfod: optimize extraction from seekable xz archives Omar Sandoval
` (3 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Omar Sandoval @ 2024-07-15 10:04 UTC (permalink / raw)
To: elfutils-devel; +Cc: linux-debuggers
From: Omar Sandoval <osandov@fb.com>
In order to extract a file from a seekable archive, we need to know
where in the uncompressed archive the file data starts and its size.
Additionally, in order to populate the response headers, we need the
file modification time (since we won't be able to get it from the
archive metadata). Add a new table, _r_seekable, keyed on the archive
file id and entry file id and containing the size, offset, and mtime.
It also contains the compression type just in case new seekable formats
are supported in the future.
In order to search this table when we get a request, we need the file
ids available. Add the ids to the _query_d and _query_e views, and
rename them to _query_d2 and _query_e2.
This schema change is backward compatible and doesn't require
reindexing. _query_d2 and _query_e2 can be renamed back the next time
BUILDIDS needs to be bumped.
Before this change, the database for a single kernel debuginfo RPM
(kernel-debuginfo-6.9.6-200.fc40.x86_64.rpm) was about 15MB. This
change increases that by about 70kB, only a 0.5% increase.
Signed-off-by: Omar Sandoval <osandov@fb.com>
---
debuginfod/debuginfod.cxx | 34 ++++++++++++++++++++++++----------
1 file changed, 24 insertions(+), 10 deletions(-)
diff --git a/debuginfod/debuginfod.cxx b/debuginfod/debuginfod.cxx
index 2d709026..81512fec 100644
--- a/debuginfod/debuginfod.cxx
+++ b/debuginfod/debuginfod.cxx
@@ -265,25 +265,39 @@ static const char DEBUGINFOD_SQLITE_DDL[] =
" foreign key (content) references " BUILDIDS "_files(id) on update cascade on delete cascade,\n"
" primary key (content, file, mtime)\n"
" ) " WITHOUT_ROWID ";\n"
+ "create table if not exists " BUILDIDS "_r_seekable (\n" // seekable rpm contents
+ " file integer not null,\n"
+ " content integer not null,\n"
+ " type text not null,\n"
+ " size integer not null,\n"
+ " offset integer not null,\n"
+ " mtime integer not null,\n"
+ " foreign key (file) references " BUILDIDS "_files(id) on update cascade on delete cascade,\n"
+ " foreign key (content) references " BUILDIDS "_files(id) on update cascade on delete cascade,\n"
+ " primary key (file, content)\n"
+ " ) " WITHOUT_ROWID ";\n"
// create views to glue together some of the above tables, for webapi D queries
- "create view if not exists " BUILDIDS "_query_d as \n"
+ // NB: _query_d2 and _query_e2 were added to replace _query_d and _query_e
+ // without updating BUILDIDS. They can be renamed back the next time BUILDIDS
+ // is updated.
+ "create view if not exists " BUILDIDS "_query_d2 as \n"
"select\n"
- " b.hex as buildid, n.mtime, 'F' as sourcetype, f0.name as source0, n.mtime as mtime, null as source1\n"
+ " b.hex as buildid, 'F' as sourcetype, n.file as id0, f0.name as source0, n.mtime as mtime, null as id1, null as source1\n"
" from " BUILDIDS "_buildids b, " BUILDIDS "_files_v f0, " BUILDIDS "_f_de n\n"
" where b.id = n.buildid and f0.id = n.file and n.debuginfo_p = 1\n"
"union all select\n"
- " b.hex as buildid, n.mtime, 'R' as sourcetype, f0.name as source0, n.mtime as mtime, f1.name as source1\n"
+ " b.hex as buildid, 'R' as sourcetype, n.file as id0, f0.name as source0, n.mtime as mtime, n.content as id1, f1.name as source1\n"
" from " BUILDIDS "_buildids b, " BUILDIDS "_files_v f0, " BUILDIDS "_files_v f1, " BUILDIDS "_r_de n\n"
" where b.id = n.buildid and f0.id = n.file and f1.id = n.content and n.debuginfo_p = 1\n"
";"
// ... and for E queries
- "create view if not exists " BUILDIDS "_query_e as \n"
+ "create view if not exists " BUILDIDS "_query_e2 as \n"
"select\n"
- " b.hex as buildid, n.mtime, 'F' as sourcetype, f0.name as source0, n.mtime as mtime, null as source1\n"
+ " b.hex as buildid, 'F' as sourcetype, n.file as id0, f0.name as source0, n.mtime as mtime, null as id1, null as source1\n"
" from " BUILDIDS "_buildids b, " BUILDIDS "_files_v f0, " BUILDIDS "_f_de n\n"
" where b.id = n.buildid and f0.id = n.file and n.executable_p = 1\n"
"union all select\n"
- " b.hex as buildid, n.mtime, 'R' as sourcetype, f0.name as source0, n.mtime as mtime, f1.name as source1\n"
+ " b.hex as buildid, 'R' as sourcetype, n.file as id0, f0.name as source0, n.mtime as mtime, n.content as id1, f1.name as source1\n"
" from " BUILDIDS "_buildids b, " BUILDIDS "_files_v f0, " BUILDIDS "_files_v f1, " BUILDIDS "_r_de n\n"
" where b.id = n.buildid and f0.id = n.file and f1.id = n.content and n.executable_p = 1\n"
";"
@@ -2557,7 +2571,7 @@ handle_buildid (MHD_Connection* conn,
if (atype_code == "D")
{
pp = new sqlite_ps (thisdb, "mhd-query-d",
- "select mtime, sourcetype, source0, source1 from " BUILDIDS "_query_d where buildid = ? "
+ "select mtime, sourcetype, source0, source1 from " BUILDIDS "_query_d2 where buildid = ? "
"order by mtime desc");
pp->reset();
pp->bind(1, buildid);
@@ -2565,7 +2579,7 @@ handle_buildid (MHD_Connection* conn,
else if (atype_code == "E")
{
pp = new sqlite_ps (thisdb, "mhd-query-e",
- "select mtime, sourcetype, source0, source1 from " BUILDIDS "_query_e where buildid = ? "
+ "select mtime, sourcetype, source0, source1 from " BUILDIDS "_query_e2 where buildid = ? "
"order by mtime desc");
pp->reset();
pp->bind(1, buildid);
@@ -2589,9 +2603,9 @@ handle_buildid (MHD_Connection* conn,
else if (atype_code == "I")
{
pp = new sqlite_ps (thisdb, "mhd-query-i",
- "select mtime, sourcetype, source0, source1, 1 as debug_p from " BUILDIDS "_query_d where buildid = ? "
+ "select mtime, sourcetype, source0, source1, 1 as debug_p from " BUILDIDS "_query_d2 where buildid = ? "
"union all "
- "select mtime, sourcetype, source0, source1, 0 as debug_p from " BUILDIDS "_query_e where buildid = ? "
+ "select mtime, sourcetype, source0, source1, 0 as debug_p from " BUILDIDS "_query_e2 where buildid = ? "
"order by debug_p desc, mtime desc");
pp->reset();
pp->bind(1, buildid);
--
2.45.2
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 3/5] debuginfod: optimize extraction from seekable xz archives
2024-07-15 10:04 [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 1/5] debuginfod: factor out common code for responding from an archive Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 2/5] debugifod: add new table and views for seekable archives Omar Sandoval
@ 2024-07-15 10:04 ` Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 4/5] debuginfod: populate _r_seekable on scan Omar Sandoval
` (2 subsequent siblings)
5 siblings, 0 replies; 10+ messages in thread
From: Omar Sandoval @ 2024-07-15 10:04 UTC (permalink / raw)
To: elfutils-devel; +Cc: linux-debuggers
From: Omar Sandoval <osandov@fb.com>
The kernel debuginfo packages on Fedora, Debian, and Ubuntu, and many of
their downstreams, are all compressed with xz in multi-threaded mode,
which allows random access. We can use this to bypass the full archive
extraction and dramatically speed up kernel debuginfo requests (from ~50
seconds in the worst case to < 0.25 seconds).
This works because multi-threaded xz compression splits up the stream
into many independently compressed blocks. The stream ends with an
index of blocks. So, to seek to an offset, we find the block containing
that offset in the index and then decompress and throw away data until
we reach the offset within the block. We can then decompress the
desired amount of data, possibly from subsequent blocks. There's no
high-level API in liblzma to do this, but we can do it by stitching
together a few low-level APIs.
We need to pass down the file ids then look up the size, uncompressed
offset, and mtime in the _r_seekable table. Note that this table is not
yet populated, so this commit has no functional change on its own.
Signed-off-by: Omar Sandoval <osandov@fb.com>
---
configure.ac | 5 +
debuginfod/Makefile.am | 2 +-
debuginfod/debuginfod.cxx | 452 +++++++++++++++++++++++++++++++++++++-
3 files changed, 453 insertions(+), 6 deletions(-)
diff --git a/configure.ac b/configure.ac
index 24e68d94..9c5f7e51 100644
--- a/configure.ac
+++ b/configure.ac
@@ -441,8 +441,13 @@ eu_ZIPLIB(bzlib,BZLIB,bz2,BZ2_bzdopen,bzip2)
# We need this since bzip2 doesn't have a pkgconfig file.
BZ2_LIB="$LIBS"
AC_SUBST([BZ2_LIB])
+save_LIBS="$LIBS"
+LIBS=
eu_ZIPLIB(lzma,LZMA,lzma,lzma_auto_decoder,[LZMA (xz)])
+lzma_LIBS="$LIBS"
+LIBS="$lzma_LIBS $save_LIBS"
AS_IF([test "x$with_lzma" = xyes], [LIBLZMA="liblzma"], [LIBLZMA=""])
+AC_SUBST([lzma_LIBS])
AC_SUBST([LIBLZMA])
eu_ZIPLIB(zstd,ZSTD,zstd,ZSTD_decompress,[ZSTD (zst)])
AS_IF([test "x$with_zstd" = xyes], [LIBZSTD="libzstd"], [LIBLZSTD=""])
diff --git a/debuginfod/Makefile.am b/debuginfod/Makefile.am
index b74e3673..e199dc0c 100644
--- a/debuginfod/Makefile.am
+++ b/debuginfod/Makefile.am
@@ -70,7 +70,7 @@ bin_PROGRAMS += debuginfod-find
endif
debuginfod_SOURCES = debuginfod.cxx
-debuginfod_LDADD = $(libdw) $(libelf) $(libeu) $(libdebuginfod) $(argp_LDADD) $(fts_LIBS) $(libmicrohttpd_LIBS) $(sqlite3_LIBS) $(libarchive_LIBS) $(rpm_LIBS) $(jsonc_LIBS) $(libcurl_LIBS) -lpthread -ldl
+debuginfod_LDADD = $(libdw) $(libelf) $(libeu) $(libdebuginfod) $(argp_LDADD) $(fts_LIBS) $(libmicrohttpd_LIBS) $(sqlite3_LIBS) $(libarchive_LIBS) $(rpm_LIBS) $(jsonc_LIBS) $(libcurl_LIBS) $(lzma_LIBS) -lpthread -ldl
debuginfod_find_SOURCES = debuginfod-find.c
debuginfod_find_LDADD = $(libdw) $(libelf) $(libeu) $(libdebuginfod) $(argp_LDADD) $(fts_LIBS) $(jsonc_LIBS)
diff --git a/debuginfod/debuginfod.cxx b/debuginfod/debuginfod.cxx
index 81512fec..a9cbd7cc 100644
--- a/debuginfod/debuginfod.cxx
+++ b/debuginfod/debuginfod.cxx
@@ -63,6 +63,10 @@ extern "C" {
#undef __attribute__ /* glibc bug - rhbz 1763325 */
#endif
+#ifdef USE_LZMA
+#include <lzma.h>
+#endif
+
#include <unistd.h>
#include <stdlib.h>
#include <locale.h>
@@ -1961,6 +1965,382 @@ handle_buildid_f_match (bool internal_req_t,
return r;
}
+
+#ifdef USE_LZMA
+struct lzma_exception: public reportable_exception
+{
+ lzma_exception(int rc, const string& msg):
+ // liblzma doesn't have a lzma_ret -> string conversion function, so just
+ // report the value.
+ reportable_exception(string ("lzma error: ") + msg + ": error " + to_string(rc)) {
+ inc_metric("error_count","lzma",to_string(rc));
+ }
+};
+
+// Neither RPM nor deb files support seeking to a specific file in the package.
+// Instead, to extract a specific file, we normally need to read the archive
+// sequentially until we find the file. This is very slow for files at the end
+// of a large package with lots of files, like kernel debuginfo.
+//
+// However, if the compression format used in the archive supports seeking, we
+// can accelerate this. As of July 2024, xz is the only widely-used format that
+// supports seeking, and usually only in multi-threaded mode. Luckily, the
+// kernel-debuginfo package in Fedora and its downstreams, and the
+// linux-image-*-dbg package in Debian and its downstreams, all happen to use
+// this.
+//
+// The xz format [1] ends with an index of independently compressed blocks in
+// the stream. In RPM and deb files, the xz stream is the last thing in the
+// file, so we assume that the xz Stream Footer is at the end of the package
+// file and do everything relative to that. For each file in the archive, we
+// remember the size and offset of the file data in the uncompressed xz stream,
+// then we use the index to seek to that offset when we need that file.
+//
+// 1: https://xz.tukaani.org/format/xz-file-format.txt
+
+// Read the Index at the end of an xz file.
+static lzma_index*
+read_xz_index (int fd)
+{
+ off_t footer_pos = -LZMA_STREAM_HEADER_SIZE;
+ if (lseek (fd, footer_pos, SEEK_END) == -1)
+ throw libc_exception (errno, "lseek");
+
+ uint8_t footer[LZMA_STREAM_HEADER_SIZE];
+ size_t footer_read = 0;
+ while (footer_read < sizeof (footer))
+ {
+ ssize_t bytes_read = read (fd, footer + footer_read,
+ sizeof (footer) - footer_read);
+ if (bytes_read < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ throw libc_exception (errno, "read");
+ }
+ if (bytes_read == 0)
+ throw reportable_exception ("truncated file");
+ footer_read += bytes_read;
+ }
+
+ lzma_stream_flags stream_flags;
+ lzma_ret ret = lzma_stream_footer_decode (&stream_flags, footer);
+ if (ret != LZMA_OK)
+ throw lzma_exception (ret, "lzma_stream_footer_decode");
+
+ if (lseek (fd, footer_pos - stream_flags.backward_size, SEEK_END) == -1)
+ throw libc_exception (errno, "lseek");
+
+ lzma_stream strm = LZMA_STREAM_INIT;
+ lzma_index* index = NULL;
+ ret = lzma_index_decoder (&strm, &index, UINT64_MAX);
+ if (ret != LZMA_OK)
+ throw lzma_exception (ret, "lzma_index_decoder");
+ defer_dtor<lzma_stream*,void> strm_ender (&strm, lzma_end);
+
+ uint8_t in_buf[4096];
+ while (true)
+ {
+ if (strm.avail_in == 0)
+ {
+ ssize_t bytes_read = read (fd, in_buf, sizeof (in_buf));
+ if (bytes_read < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ throw libc_exception (errno, "read");
+ }
+ if (bytes_read == 0)
+ throw reportable_exception ("truncated file");
+ strm.avail_in = bytes_read;
+ strm.next_in = in_buf;
+ }
+
+ ret = lzma_code (&strm, LZMA_RUN);
+ if (ret == LZMA_STREAM_END)
+ break;
+ else if (ret != LZMA_OK)
+ throw lzma_exception (ret, "lzma_code index");
+ }
+
+ ret = lzma_index_stream_flags (index, &stream_flags);
+ if (ret != LZMA_OK)
+ {
+ lzma_index_end (index, NULL);
+ throw lzma_exception (ret, "lzma_index_stream_flags");
+ }
+ return index;
+}
+
+static void
+my_lzma_index_end (lzma_index* index)
+{
+ lzma_index_end (index, NULL);
+}
+
+static void
+free_lzma_block_filter_options (lzma_block* block)
+{
+ for (int i = 0; i < LZMA_FILTERS_MAX; i++)
+ {
+ free (block->filters[i].options);
+ block->filters[i].options = NULL;
+ }
+}
+
+static void
+free_lzma_block_filters (lzma_block* block)
+{
+ if (block->filters != NULL)
+ {
+ free_lzma_block_filter_options (block);
+ free (block->filters);
+ }
+}
+
+static void
+extract_xz_blocks_into_fd (const string& srcpath,
+ int src,
+ int dst,
+ lzma_index_iter* iter,
+ uint64_t offset,
+ uint64_t size)
+{
+ // Seek to the Block. Seeking from the end using the compressed size from the
+ // footer means we don't need to know where the xz stream starts in the
+ // archive.
+ if (lseek (src,
+ (off_t) iter->block.compressed_stream_offset
+ - (off_t) iter->stream.compressed_size,
+ SEEK_END) == -1)
+ throw libc_exception (errno, "lseek");
+
+ offset -= iter->block.uncompressed_file_offset;
+
+ lzma_block block{};
+ block.filters = (lzma_filter*) calloc (LZMA_FILTERS_MAX + 1,
+ sizeof (lzma_filter));
+ if (block.filters == NULL)
+ throw libc_exception (ENOMEM, "cannot allocate lzma_block filters");
+ defer_dtor<lzma_block*,void> filters_freer (&block, free_lzma_block_filters);
+
+ uint8_t in_buf[4096];
+ uint8_t out_buf[4096];
+ size_t header_read = 0;
+ bool need_log_extracting = verbose > 3;
+ while (true)
+ {
+ // The first byte of the Block is the encoded Block Header Size. Read the
+ // first byte and whatever extra fits in the buffer.
+ while (header_read == 0)
+ {
+ ssize_t bytes_read = read (src, in_buf, sizeof (in_buf));
+ if (bytes_read < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ throw libc_exception (errno, "read");
+ }
+ if (bytes_read == 0)
+ throw reportable_exception ("truncated file");
+ header_read += bytes_read;
+ }
+
+ block.header_size = lzma_block_header_size_decode (in_buf[0]);
+
+ // If we didn't buffer the whole Block Header earlier, get the rest.
+ eu_static_assert (sizeof (in_buf)
+ >= lzma_block_header_size_decode (UINT8_MAX));
+ while (header_read < block.header_size)
+ {
+ ssize_t bytes_read = read (src, in_buf + header_read,
+ sizeof (in_buf) - header_read);
+ if (bytes_read < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ throw libc_exception (errno, "read");
+ }
+ if (bytes_read == 0)
+ throw reportable_exception ("truncated file");
+ header_read += bytes_read;
+ }
+
+ // Decode the Block Header.
+ block.check = iter->stream.flags->check;
+ lzma_ret ret = lzma_block_header_decode (&block, NULL, in_buf);
+ if (ret != LZMA_OK)
+ throw lzma_exception (ret, "lzma_block_header_decode");
+ ret = lzma_block_compressed_size (&block, iter->block.unpadded_size);
+ if (ret != LZMA_OK)
+ throw lzma_exception (ret, "lzma_block_compressed_size");
+
+ // Start decoding the Block data.
+ lzma_stream strm = LZMA_STREAM_INIT;
+ ret = lzma_block_decoder (&strm, &block);
+ if (ret != LZMA_OK)
+ throw lzma_exception (ret, "lzma_block_decoder");
+ defer_dtor<lzma_stream*,void> strm_ender (&strm, lzma_end);
+
+ // We might still have some input buffered from when we read the header.
+ strm.avail_in = header_read - block.header_size;
+ strm.next_in = in_buf + block.header_size;
+ strm.avail_out = sizeof (out_buf);
+ strm.next_out = out_buf;
+ while (true)
+ {
+ if (strm.avail_in == 0)
+ {
+ ssize_t bytes_read = read (src, in_buf, sizeof (in_buf));
+ if (bytes_read < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ throw libc_exception (errno, "read");
+ }
+ if (bytes_read == 0)
+ throw reportable_exception ("truncated file");
+ strm.avail_in = bytes_read;
+ strm.next_in = in_buf;
+ }
+
+ ret = lzma_code (&strm, LZMA_RUN);
+ if (ret != LZMA_OK && ret != LZMA_STREAM_END)
+ throw lzma_exception (ret, "lzma_code block");
+
+ // Throw away anything we decode until we reach the offset, then
+ // start writing to the destination.
+ if (strm.total_out > offset)
+ {
+ size_t bytes_to_write = strm.next_out - out_buf;
+ uint8_t* buf_to_write = out_buf;
+
+ // Ignore anything in the buffer before the offset.
+ if (bytes_to_write > strm.total_out - offset)
+ {
+ buf_to_write += bytes_to_write - (strm.total_out - offset);
+ bytes_to_write = strm.total_out - offset;
+ }
+
+ // Ignore anything after the size.
+ if (strm.total_out - offset >= size)
+ bytes_to_write -= strm.total_out - offset - size;
+
+ if (need_log_extracting)
+ {
+ obatched(clog) << "extracting from xz archive " << srcpath
+ << " size=" << size << endl;
+ need_log_extracting = false;
+ }
+
+ while (bytes_to_write > 0)
+ {
+ ssize_t written = write (dst, buf_to_write, bytes_to_write);
+ if (written < 0)
+ {
+ if (errno == EAGAIN)
+ continue;
+ throw libc_exception (errno, "write");
+ }
+ bytes_to_write -= written;
+ buf_to_write += written;
+ }
+
+ // If we reached the size, we're done.
+ if (strm.total_out - offset >= size)
+ return;
+ }
+
+ strm.avail_out = sizeof (out_buf);
+ strm.next_out = out_buf;
+
+ if (ret == LZMA_STREAM_END)
+ break;
+ }
+
+ // This Block didn't have enough data. Go to the next one.
+ if (lzma_index_iter_next (iter, LZMA_INDEX_ITER_BLOCK))
+ throw reportable_exception ("no more blocks");
+ if (strm.total_out > offset)
+ size -= strm.total_out - offset;
+ offset = 0;
+ // If we had any buffered input left, move it to the beginning of the
+ // buffer to decode the next Block Header.
+ if (strm.avail_in > 0)
+ {
+ memmove (in_buf, strm.next_in, strm.avail_in);
+ header_read = strm.avail_in;
+ }
+ else
+ header_read = 0;
+ free_lzma_block_filter_options (&block);
+ }
+}
+
+static int
+extract_from_seekable_archive (const string& srcpath,
+ char* tmppath,
+ uint64_t offset,
+ uint64_t size)
+{
+ int src = open (srcpath.c_str(), O_RDONLY);
+ if (src < 0)
+ throw libc_exception (errno, string("open ") + srcpath);
+ defer_dtor<int,int> src_closer (src, close);
+
+ try
+ {
+ lzma_index* index = read_xz_index (src);
+ defer_dtor<lzma_index*,void> index_ender (index, my_lzma_index_end);
+
+ // Find the Block containing the offset.
+ lzma_index_iter iter;
+ lzma_index_iter_init (&iter, index);
+ if (lzma_index_iter_locate (&iter, offset))
+ throw reportable_exception ("offset not found");
+
+ if (verbose > 3)
+ obatched(clog) << "seeking in xz archive " << srcpath
+ << " offset=" << offset << " block_offset="
+ << iter.block.uncompressed_file_offset << endl;
+
+ int dst = mkstemp (tmppath);
+ if (dst < 0)
+ throw libc_exception (errno, "cannot create temporary file");
+
+ try
+ {
+ extract_xz_blocks_into_fd (srcpath, src, dst, &iter, offset, size);
+ }
+ catch (...)
+ {
+ unlink (tmppath);
+ close (dst);
+ throw;
+ }
+
+ return dst;
+ }
+ catch (const reportable_exception &e)
+ {
+ if (verbose)
+ obatched(clog) << "failed to extract from seekable archive " << srcpath
+ << ": " << e.message << endl;
+ return -1;
+ }
+}
+#else
+static int
+extract_from_seekable_archive (const string& srcpath,
+ char* tmppath,
+ uint64_t offset,
+ uint64_t size)
+{
+ return -1;
+}
+#endif
+
+
// For security/portability reasons, many distro-package archives have
// a "./" in front of path names; others have nothing, others have
// "/". Canonicalize them all to a single leading "/", with the
@@ -2060,6 +2440,8 @@ handle_buildid_r_match (bool internal_req_p,
int64_t b_mtime,
const string& b_source0,
const string& b_source1,
+ int64_t b_id0,
+ int64_t b_id1,
const string& section,
int *result_fd)
{
@@ -2246,7 +2628,58 @@ handle_buildid_r_match (bool internal_req_p,
// NB: see, we never go around the 'loop' more than once
}
- // no match ... grumble, must process the archive
+ // no match ... look for a seekable entry
+ unique_ptr<sqlite_ps> pp (new sqlite_ps (db, "rpm-seekable-query",
+ "select type, size, offset, mtime from " BUILDIDS "_r_seekable "
+ "where file = ? and content = ?"));
+ rc = pp->reset().bind(1, b_id0).bind(2, b_id1).step();
+ if (rc != SQLITE_DONE)
+ {
+ if (rc != SQLITE_ROW)
+ throw sqlite_exception(rc, "step");
+ const char* seekable_type = (const char*) sqlite3_column_text (*pp, 0);
+ if (seekable_type != NULL && strcmp (seekable_type, "xz") == 0)
+ {
+ int64_t seekable_size = sqlite3_column_int64 (*pp, 1);
+ int64_t seekable_offset = sqlite3_column_int64 (*pp, 2);
+ int64_t seekable_mtime = sqlite3_column_int64 (*pp, 3);
+
+ char* tmppath = NULL;
+ if (asprintf (&tmppath, "%s/debuginfod-fdcache.XXXXXX", tmpdir.c_str()) < 0)
+ throw libc_exception (ENOMEM, "cannot allocate tmppath");
+ defer_dtor<void*,void> tmmpath_freer (tmppath, free);
+
+ fd = extract_from_seekable_archive (b_source0, tmppath,
+ seekable_offset, seekable_size);
+ if (fd >= 0)
+ {
+ // Set the mtime so the fdcache file mtimes propagate to future webapi
+ // clients.
+ struct timespec tvs[2];
+ tvs[0].tv_sec = 0;
+ tvs[0].tv_nsec = UTIME_OMIT;
+ tvs[1].tv_sec = seekable_mtime;
+ tvs[1].tv_nsec = 0;
+ (void) futimens (fd, tvs); /* best effort */
+ struct MHD_Response* r = create_buildid_r_response (b_mtime,
+ b_source0,
+ b_source1,
+ section,
+ ima_sig,
+ tmppath, fd,
+ seekable_size,
+ seekable_mtime,
+ "seekable archive",
+ extract_begin);
+ if (r != 0 && result_fd)
+ *result_fd = fd;
+ return r;
+ }
+ }
+ }
+ pp.reset();
+
+ // still no match ... grumble, must process the archive
string archive_decoder = "/dev/null";
string archive_extension = "";
for (auto&& arch : scan_archives)
@@ -2445,6 +2878,8 @@ handle_buildid_match (bool internal_req_p,
const string& b_stype,
const string& b_source0,
const string& b_source1,
+ int64_t b_id0,
+ int64_t b_id1,
const string& section,
int *result_fd)
{
@@ -2455,7 +2890,8 @@ handle_buildid_match (bool internal_req_p,
section, result_fd);
else if (b_stype == "R")
return handle_buildid_r_match(internal_req_p, b_mtime, b_source0,
- b_source1, section, result_fd);
+ b_source1, b_id0, b_id1, section,
+ result_fd);
}
catch (const reportable_exception &e)
{
@@ -2571,7 +3007,7 @@ handle_buildid (MHD_Connection* conn,
if (atype_code == "D")
{
pp = new sqlite_ps (thisdb, "mhd-query-d",
- "select mtime, sourcetype, source0, source1 from " BUILDIDS "_query_d2 where buildid = ? "
+ "select mtime, sourcetype, source0, source1, id0, id1 from " BUILDIDS "_query_d2 where buildid = ? "
"order by mtime desc");
pp->reset();
pp->bind(1, buildid);
@@ -2579,7 +3015,7 @@ handle_buildid (MHD_Connection* conn,
else if (atype_code == "E")
{
pp = new sqlite_ps (thisdb, "mhd-query-e",
- "select mtime, sourcetype, source0, source1 from " BUILDIDS "_query_e2 where buildid = ? "
+ "select mtime, sourcetype, source0, source1, id0, id1 from " BUILDIDS "_query_e2 where buildid = ? "
"order by mtime desc");
pp->reset();
pp->bind(1, buildid);
@@ -2627,6 +3063,12 @@ handle_buildid (MHD_Connection* conn,
string b_stype = string((const char*) sqlite3_column_text (*pp, 1) ?: ""); /* by DDL may not be NULL */
string b_source0 = string((const char*) sqlite3_column_text (*pp, 2) ?: ""); /* may be NULL */
string b_source1 = string((const char*) sqlite3_column_text (*pp, 3) ?: ""); /* may be NULL */
+ int64_t b_id0 = 0, b_id1 = 0;
+ if (atype_code == "D" || atype_code == "E")
+ {
+ b_id0 = sqlite3_column_int64 (*pp, 4);
+ b_id1 = sqlite3_column_int64 (*pp, 5);
+ }
if (verbose > 1)
obatched(clog) << "found mtime=" << b_mtime << " stype=" << b_stype
@@ -2636,7 +3078,7 @@ handle_buildid (MHD_Connection* conn,
// XXX: in case of multiple matches, attempt them in parallel?
auto r = handle_buildid_match (conn ? false : true,
b_mtime, b_stype, b_source0, b_source1,
- section, result_fd);
+ b_id0, b_id1, section, result_fd);
if (r)
return r;
--
2.45.2
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 4/5] debuginfod: populate _r_seekable on scan
2024-07-15 10:04 [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Omar Sandoval
` (2 preceding siblings ...)
2024-07-15 10:04 ` [PATCH v2 3/5] debuginfod: optimize extraction from seekable xz archives Omar Sandoval
@ 2024-07-15 10:04 ` Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 5/5] debuginfod: populate _r_seekable on request Omar Sandoval
2024-07-16 20:16 ` [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Frank Ch. Eigler
5 siblings, 0 replies; 10+ messages in thread
From: Omar Sandoval @ 2024-07-15 10:04 UTC (permalink / raw)
To: elfutils-devel; +Cc: linux-debuggers
From: Omar Sandoval <osandov@fb.com>
Whenever a new archive is scanned, check if it is seekable with a little
liblzma magic, and populate _r_seekable if so. With this, newly scanned
seekable archives will used the optimized extraction path added in the
previous commit.
Signed-off-by: Omar Sandoval <osandov@fb.com>
---
debuginfod/debuginfod.cxx | 150 +++++++++++++++++++++++++++++++++++++-
1 file changed, 147 insertions(+), 3 deletions(-)
diff --git a/debuginfod/debuginfod.cxx b/debuginfod/debuginfod.cxx
index a9cbd7cc..f120dc90 100644
--- a/debuginfod/debuginfod.cxx
+++ b/debuginfod/debuginfod.cxx
@@ -1998,6 +1998,109 @@ struct lzma_exception: public reportable_exception
//
// 1: https://xz.tukaani.org/format/xz-file-format.txt
+// Return whether an archive supports seeking.
+static bool
+is_seekable_archive (const string& rps, struct archive* a)
+{
+ // Only xz supports seeking.
+ if (archive_filter_code (a, 0) != ARCHIVE_FILTER_XZ)
+ return false;
+
+ int fd = open (rps.c_str(), O_RDONLY);
+ if (fd < 0)
+ return false;
+ defer_dtor<int,int> fd_closer (fd, close);
+
+ // Seek to the xz Stream Footer. We assume that it's the last thing in the
+ // file, which is true for RPM and deb files.
+ off_t footer_pos = -LZMA_STREAM_HEADER_SIZE;
+ if (lseek (fd, footer_pos, SEEK_END) == -1)
+ return false;
+
+ // Decode the Stream Footer.
+ uint8_t footer[LZMA_STREAM_HEADER_SIZE];
+ size_t footer_read = 0;
+ while (footer_read < sizeof (footer))
+ {
+ ssize_t bytes_read = read (fd, footer + footer_read,
+ sizeof (footer) - footer_read);
+ if (bytes_read < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ return false;
+ }
+ if (bytes_read == 0)
+ return false;
+ footer_read += bytes_read;
+ }
+
+ lzma_stream_flags stream_flags;
+ lzma_ret ret = lzma_stream_footer_decode (&stream_flags, footer);
+ if (ret != LZMA_OK)
+ return false;
+
+ // Seek to the xz Index.
+ if (lseek (fd, footer_pos - stream_flags.backward_size, SEEK_END) == -1)
+ return false;
+
+ // Decode the Number of Records in the Index. liblzma doesn't have an API for
+ // this if you don't want to decode the whole Index, so we have to do it
+ // ourselves.
+ //
+ // We need 1 byte for the Index Indicator plus 1-9 bytes for the
+ // variable-length integer Number of Records.
+ uint8_t index[10];
+ size_t index_read = 0;
+ while (index_read == 0) {
+ ssize_t bytes_read = read (fd, index, sizeof (index));
+ if (bytes_read < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ return false;
+ }
+ if (bytes_read == 0)
+ return false;
+ index_read += bytes_read;
+ }
+ // The Index Indicator must be 0.
+ if (index[0] != 0)
+ return false;
+
+ lzma_vli num_records;
+ size_t pos = 0;
+ size_t in_pos = 1;
+ while (true)
+ {
+ if (in_pos >= index_read)
+ {
+ ssize_t bytes_read = read (fd, index, sizeof (index));
+ if (bytes_read < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ return false;
+ }
+ if (bytes_read == 0)
+ return false;
+ index_read = bytes_read;
+ in_pos = 0;
+ }
+ ret = lzma_vli_decode (&num_records, &pos, index, &in_pos, index_read);
+ if (ret == LZMA_STREAM_END)
+ break;
+ else if (ret != LZMA_OK)
+ return false;
+ }
+
+ if (verbose > 3)
+ obatched(clog) << rps << " has " << num_records << " xz Blocks" << endl;
+
+ // The file is only seekable if it has more than one Block.
+ return num_records > 1;
+}
+
// Read the Index at the end of an xz file.
static lzma_index*
read_xz_index (int fd)
@@ -2330,6 +2433,11 @@ extract_from_seekable_archive (const string& srcpath,
}
}
#else
+static bool
+is_seekable_archive (const string& rps, struct archive* a)
+{
+ return false;
+}
static int
extract_from_seekable_archive (const string& srcpath,
char* tmppath,
@@ -4277,6 +4385,7 @@ archive_classify (const string& rps, string& archive_extension, int64_t archivei
sqlite_ps& ps_upsert_buildids, sqlite_ps& ps_upsert_fileparts, sqlite_ps& ps_upsert_file,
sqlite_ps& ps_lookup_file,
sqlite_ps& ps_upsert_de, sqlite_ps& ps_upsert_sref, sqlite_ps& ps_upsert_sdef,
+ sqlite_ps& ps_upsert_seekable,
time_t mtime,
unsigned& fts_executable, unsigned& fts_debuginfo, unsigned& fts_sref, unsigned& fts_sdef,
bool& fts_sref_complete_p)
@@ -4331,6 +4440,10 @@ archive_classify (const string& rps, string& archive_extension, int64_t archivei
if (verbose > 3)
obatched(clog) << "libarchive scanning " << rps << " id " << archiveid << endl;
+ bool seekable = is_seekable_archive (rps, a);
+ if (verbose> 2 && seekable)
+ obatched(clog) << rps << " is seekable" << endl;
+
bool any_exceptions = false;
while(1) // parse archive entries
{
@@ -4352,6 +4465,15 @@ archive_classify (const string& rps, string& archive_extension, int64_t archivei
if (verbose > 3)
obatched(clog) << "libarchive checking " << fn << endl;
+ int64_t seekable_size, seekable_offset;
+ time_t seekable_mtime;
+ if (seekable)
+ {
+ seekable_size = archive_entry_size (e);
+ seekable_offset = archive_filter_bytes (a, 0);
+ seekable_mtime = archive_entry_mtime (e);
+ }
+
// extract this file to a temporary file
char* tmppath = NULL;
rc = asprintf (&tmppath, "%s/debuginfod-classify.XXXXXX", tmpdir.c_str());
@@ -4443,6 +4565,15 @@ archive_classify (const string& rps, string& archive_extension, int64_t archivei
.bind(5, mtime)
.bind(6, fileid)
.step_ok_done();
+ if (seekable)
+ ps_upsert_seekable
+ .reset()
+ .bind(1, archiveid)
+ .bind(2, fileid)
+ .bind(3, seekable_size)
+ .bind(4, seekable_offset)
+ .bind(5, seekable_mtime)
+ .step_ok_done();
}
else // potential source - sdef record
{
@@ -4456,11 +4587,19 @@ archive_classify (const string& rps, string& archive_extension, int64_t archivei
}
if ((verbose > 2) && (executable_p || debuginfo_p))
- obatched(clog) << "recorded buildid=" << buildid << " rpm=" << rps << " file=" << fn
+ {
+ obatched ob(clog);
+ auto& o = ob << "recorded buildid=" << buildid << " rpm=" << rps << " file=" << fn
<< " mtime=" << mtime << " atype="
<< (executable_p ? "E" : "")
<< (debuginfo_p ? "D" : "")
- << " sourcefiles=" << sourcefiles.size() << endl;
+ << " sourcefiles=" << sourcefiles.size();
+ if (seekable)
+ o << " seekable size=" << seekable_size
+ << " offset=" << seekable_offset
+ << " mtime=" << seekable_mtime;
+ o << endl;
+ }
}
catch (const reportable_exception& e)
@@ -4491,6 +4630,7 @@ scan_archive_file (const string& rps, const stat_t& st,
sqlite_ps& ps_upsert_de,
sqlite_ps& ps_upsert_sref,
sqlite_ps& ps_upsert_sdef,
+ sqlite_ps& ps_upsert_seekable,
sqlite_ps& ps_query,
sqlite_ps& ps_scan_done,
unsigned& fts_cached,
@@ -4528,7 +4668,7 @@ scan_archive_file (const string& rps, const stat_t& st,
string archive_extension;
archive_classify (rps, archive_extension, archiveid,
ps_upsert_buildids, ps_upsert_fileparts, ps_upsert_file, ps_lookup_file,
- ps_upsert_de, ps_upsert_sref, ps_upsert_sdef, // dalt
+ ps_upsert_de, ps_upsert_sref, ps_upsert_sdef, ps_upsert_seekable, // dalt
st.st_mtime,
my_fts_executable, my_fts_debuginfo, my_fts_sref, my_fts_sdef,
my_fts_sref_complete_p);
@@ -4634,6 +4774,9 @@ scan ()
sqlite_ps ps_r_upsert_sdef (db, "rpm-sdef-insert",
"insert or ignore into " BUILDIDS "_r_sdef (file, mtime, content) values ("
"?, ?, ?);");
+ sqlite_ps ps_r_upsert_seekable (db, "rpm-seekable-insert",
+ "insert or ignore into " BUILDIDS "_r_seekable (file, content, type, size, offset, mtime) "
+ "values (?, ?, 'xz', ?, ?, ?);");
sqlite_ps ps_r_query (db, "rpm-negativehit-query",
"select 1 from " BUILDIDS "_file_mtime_scanned where "
"sourcetype = 'R' and file = ? and mtime = ?;");
@@ -4676,6 +4819,7 @@ scan ()
ps_r_upsert_de,
ps_r_upsert_sref,
ps_r_upsert_sdef,
+ ps_r_upsert_seekable,
ps_r_query,
ps_r_scan_done,
fts_cached,
--
2.45.2
^ permalink raw reply related [flat|nested] 10+ messages in thread
* [PATCH v2 5/5] debuginfod: populate _r_seekable on request
2024-07-15 10:04 [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Omar Sandoval
` (3 preceding siblings ...)
2024-07-15 10:04 ` [PATCH v2 4/5] debuginfod: populate _r_seekable on scan Omar Sandoval
@ 2024-07-15 10:04 ` Omar Sandoval
2024-07-16 20:16 ` [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Frank Ch. Eigler
5 siblings, 0 replies; 10+ messages in thread
From: Omar Sandoval @ 2024-07-15 10:04 UTC (permalink / raw)
To: elfutils-devel; +Cc: linux-debuggers
From: Omar Sandoval <osandov@fb.com>
Since the schema change adding _r_seekable was done in a backward
compatible way, seekable archives that were previously scanned will not
be in _r_seekable. Whenever an archive is going to be extracted to
satisfy a request, check if it is seekable. If so, populate _r_seekable
while extracting it so that future requests use the optimized path.
The next time that BUILDIDS is bumped, all archives will be checked at
scan time. At that point, checking again will be unnecessary and this
commit can be reverted.
Signed-off-by: Omar Sandoval <osandov@fb.com>
---
debuginfod/debuginfod.cxx | 76 +++++++++++++++++++++++++++++++++++----
1 file changed, 70 insertions(+), 6 deletions(-)
diff --git a/debuginfod/debuginfod.cxx b/debuginfod/debuginfod.cxx
index f120dc90..6fb4627c 100644
--- a/debuginfod/debuginfod.cxx
+++ b/debuginfod/debuginfod.cxx
@@ -2737,6 +2737,7 @@ handle_buildid_r_match (bool internal_req_p,
}
// no match ... look for a seekable entry
+ bool populate_seekable = true;
unique_ptr<sqlite_ps> pp (new sqlite_ps (db, "rpm-seekable-query",
"select type, size, offset, mtime from " BUILDIDS "_r_seekable "
"where file = ? and content = ?"));
@@ -2745,6 +2746,9 @@ handle_buildid_r_match (bool internal_req_p,
{
if (rc != SQLITE_ROW)
throw sqlite_exception(rc, "step");
+ // if we found a match in _r_seekable but we fail to extract it, don't
+ // bother populating it again
+ populate_seekable = false;
const char* seekable_type = (const char*) sqlite3_column_text (*pp, 0);
if (seekable_type != NULL && strcmp (seekable_type, "xz") == 0)
{
@@ -2836,16 +2840,39 @@ handle_buildid_r_match (bool internal_req_p,
throw archive_exception(a, "cannot open archive from pipe");
}
- // archive traversal is in three stages, no, four stages:
- // 1) skip entries whose names do not match the requested one
- // 2) extract the matching entry name (set r = result)
- // 3) extract some number of prefetched entries (just into fdcache)
- // 4) abort any further processing
+ // If the archive was scanned in a version without _r_seekable, then we may
+ // need to populate _r_seekable now. This can be removed the next time
+ // BUILDIDS is updated.
+ if (populate_seekable)
+ {
+ populate_seekable = is_seekable_archive (b_source0, a);
+ if (populate_seekable)
+ {
+ // NB: the names are already interned
+ pp.reset(new sqlite_ps (db, "rpm-seekable-insert2",
+ "insert or ignore into " BUILDIDS "_r_seekable (file, content, type, size, offset, mtime) "
+ "values (?, "
+ "(select id from " BUILDIDS "_files "
+ "where dirname = (select id from " BUILDIDS "_fileparts where name = ?) "
+ "and basename = (select id from " BUILDIDS "_fileparts where name = ?) "
+ "), 'xz', ?, ?, ?)"));
+ }
+ }
+
+ // archive traversal is in five stages:
+ // 1) before we find a matching entry, insert it into _r_seekable if needed or
+ // skip it otherwise
+ // 2) extract the matching entry (set r = result). Also insert it into
+ // _r_seekable if needed
+ // 3) extract some number of prefetched entries (just into fdcache). Also
+ // insert them into _r_seekable if needed
+ // 4) if needed, insert all of the remaining entries into _r_seekable
+ // 5) abort any further processing
struct MHD_Response* r = 0; // will set in stage 2
unsigned prefetch_count =
internal_req_p ? 0 : fdcache_prefetch; // will decrement in stage 3
- while(r == 0 || prefetch_count > 0) // stage 1, 2, or 3
+ while(r == 0 || prefetch_count > 0 || populate_seekable) // stage 1-4
{
if (interrupted)
break;
@@ -2859,6 +2886,43 @@ handle_buildid_r_match (bool internal_req_p,
continue;
string fn = canonicalized_archive_entry_pathname (e);
+
+ if (populate_seekable)
+ {
+ string dn, bn;
+ size_t slash = fn.rfind('/');
+ if (slash == std::string::npos) {
+ dn = "";
+ bn = fn;
+ } else {
+ dn = fn.substr(0, slash);
+ bn = fn.substr(slash + 1);
+ }
+
+ int64_t seekable_size = archive_entry_size (e);
+ int64_t seekable_offset = archive_filter_bytes (a, 0);
+ time_t seekable_mtime = archive_entry_mtime (e);
+
+ pp->reset();
+ pp->bind(1, b_id0);
+ pp->bind(2, dn);
+ pp->bind(3, bn);
+ pp->bind(4, seekable_size);
+ pp->bind(5, seekable_offset);
+ pp->bind(6, seekable_mtime);
+ rc = pp->step();
+ if (rc != SQLITE_DONE)
+ obatched(clog) << "recording seekable file=" << fn
+ << " sqlite3 error: " << (sqlite3_errstr(rc) ?: "?") << endl;
+ else if (verbose > 2)
+ obatched(clog) << "recorded seekable file=" << fn
+ << " size=" << seekable_size
+ << " offset=" << seekable_offset
+ << " mtime=" << seekable_mtime << endl;
+ if (r != 0 && prefetch_count == 0) // stage 4
+ continue;
+ }
+
if ((r == 0) && (fn != b_source1)) // stage 1
continue;
--
2.45.2
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x
2024-07-15 10:04 [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Omar Sandoval
` (4 preceding siblings ...)
2024-07-15 10:04 ` [PATCH v2 5/5] debuginfod: populate _r_seekable on request Omar Sandoval
@ 2024-07-16 20:16 ` Frank Ch. Eigler
2024-07-16 21:15 ` Omar Sandoval
2024-07-16 22:15 ` Frank Ch. Eigler
5 siblings, 2 replies; 10+ messages in thread
From: Frank Ch. Eigler @ 2024-07-16 20:16 UTC (permalink / raw)
To: Omar Sandoval; +Cc: elfutils-devel, linux-debuggers
Hi -
> This is v2 of my patch series optimizing debuginfod for kernel
> debuginfo. v1 is here [7].
This generally looks great to me. I'll send it through the testsuite
trybots here. But there isn't an xz-y test case yet. Is there a
smallish seekable-xz rpm file that you have or could make that we
could jam into the elfutils testsuite? (A prometheus metric counting
seeked-xz attempts/successes would be cool to assist testing and to
monitor it on deployed servers.)
- FChE
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x
2024-07-16 20:16 ` [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Frank Ch. Eigler
@ 2024-07-16 21:15 ` Omar Sandoval
2024-07-16 22:15 ` Frank Ch. Eigler
1 sibling, 0 replies; 10+ messages in thread
From: Omar Sandoval @ 2024-07-16 21:15 UTC (permalink / raw)
To: Frank Ch. Eigler; +Cc: elfutils-devel, linux-debuggers
On Tue, Jul 16, 2024 at 04:16:01PM -0400, Frank Ch. Eigler wrote:
> Hi -
>
> > This is v2 of my patch series optimizing debuginfod for kernel
> > debuginfo. v1 is here [7].
>
> This generally looks great to me. I'll send it through the testsuite
> trybots here.
Great, thank you!
> But there isn't an xz-y test case yet. Is there a
> smallish seekable-xz rpm file that you have or could make that we
> could jam into the elfutils testsuite?
Good point, I'll add one.
> (A prometheus metric counting
> seeked-xz attempts/successes would be cool to assist testing and to
> monitor it on deployed servers.)
I do have:
inc_metric("error_count","lzma",to_string(rc));
in lzma_exception and:
inc_metric ("http_responses_total","result",metric);
in create_buildid_r_response, which gets called with
metric = "seekable archive". But I'll add another metric for attempts,
and name the existing one "seekable xz archive" to be more explicit.
Thanks,
Omar
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x
2024-07-16 20:16 ` [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Frank Ch. Eigler
2024-07-16 21:15 ` Omar Sandoval
@ 2024-07-16 22:15 ` Frank Ch. Eigler
2024-07-16 22:17 ` Omar Sandoval
1 sibling, 1 reply; 10+ messages in thread
From: Frank Ch. Eigler @ 2024-07-16 22:15 UTC (permalink / raw)
To: Frank Ch. Eigler; +Cc: Omar Sandoval, elfutils-devel, linux-debuggers
Hi -
> [...] I'll send it through the testsuite
> trybots here. [...]
There was some success and there was some failure. :-)
all 11 runs:
https://builder.sourceware.org/testruns/?commitishes=&has_expfile_glob=&has_trsfile_glob=&has_keyvalue_k=testrun.git_describe&has_keyvalue_op=glob&has_keyvalue_v=*&has_keyvalue2_k=source.gitbranch&has_keyvalue2_op=glob&has_keyvalue2_v=*users%2Ffche%2Ftry-xz*&order_by=testrun.authored.time&order=desc
in a grid view:
https://builder.sourceware.org/r_grid_testcase/?trid=9b0340db2a771c5b6483132afd75139699c4f8e5&toplevel=True&vertical=source.gitdescribe&v_limit=5&horizontal=uname-m&h_limit=10&opt_keyword_key=source.gitbranch&opt_keyword_value=*fche%2Ftry-xz*
e.g. failure:
https://builder.sourceware.org/testrun/2550ae9a06868d7e7f4c62176960c04f4534d8d0?filename=tests%2Frun-debuginfod-extraction-passive.sh.log#line657
- FChE
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x
2024-07-16 22:15 ` Frank Ch. Eigler
@ 2024-07-16 22:17 ` Omar Sandoval
0 siblings, 0 replies; 10+ messages in thread
From: Omar Sandoval @ 2024-07-16 22:17 UTC (permalink / raw)
To: Frank Ch. Eigler; +Cc: elfutils-devel, linux-debuggers
On Tue, Jul 16, 2024 at 06:15:16PM -0400, Frank Ch. Eigler wrote:
> Hi -
>
> > [...] I'll send it through the testsuite
> > trybots here. [...]
>
> There was some success and there was some failure. :-)
>
> all 11 runs:
>
> https://builder.sourceware.org/testruns/?commitishes=&has_expfile_glob=&has_trsfile_glob=&has_keyvalue_k=testrun.git_describe&has_keyvalue_op=glob&has_keyvalue_v=*&has_keyvalue2_k=source.gitbranch&has_keyvalue2_op=glob&has_keyvalue2_v=*users%2Ffche%2Ftry-xz*&order_by=testrun.authored.time&order=desc
>
> in a grid view:
>
> https://builder.sourceware.org/r_grid_testcase/?trid=9b0340db2a771c5b6483132afd75139699c4f8e5&toplevel=True&vertical=source.gitdescribe&v_limit=5&horizontal=uname-m&h_limit=10&opt_keyword_key=source.gitbranch&opt_keyword_value=*fche%2Ftry-xz*
>
> e.g. failure:
>
> https://builder.sourceware.org/testrun/2550ae9a06868d7e7f4c62176960c04f4534d8d0?filename=tests%2Frun-debuginfod-extraction-passive.sh.log#line657
Yup, that was a goof, and I don't know why I missed it on my local test
run. This is the fix:
diff --git a/debuginfod/debuginfod.cxx b/debuginfod/debuginfod.cxx
index 6fb4627c..08114f2e 100644
--- a/debuginfod/debuginfod.cxx
+++ b/debuginfod/debuginfod.cxx
@@ -2737,8 +2737,9 @@ handle_buildid_r_match (bool internal_req_p,
}
// no match ... look for a seekable entry
- bool populate_seekable = true;
- unique_ptr<sqlite_ps> pp (new sqlite_ps (db, "rpm-seekable-query",
+ bool populate_seekable = ! passive_p;
+ unique_ptr<sqlite_ps> pp (new sqlite_ps (internal_req_p ? db : dbq,
+ "rpm-seekable-query",
"select type, size, offset, mtime from " BUILDIDS "_r_seekable "
"where file = ? and content = ?"));
rc = pp->reset().bind(1, b_id0).bind(2, b_id1).step();
I'll fold that in and include it in the next version.
^ permalink raw reply related [flat|nested] 10+ messages in thread
end of thread, other threads:[~2024-07-16 22:17 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-07-15 10:04 [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 1/5] debuginfod: factor out common code for responding from an archive Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 2/5] debugifod: add new table and views for seekable archives Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 3/5] debuginfod: optimize extraction from seekable xz archives Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 4/5] debuginfod: populate _r_seekable on scan Omar Sandoval
2024-07-15 10:04 ` [PATCH v2 5/5] debuginfod: populate _r_seekable on request Omar Sandoval
2024-07-16 20:16 ` [PATCH v2 0/5] debuginfod: speed up extraction from kernel debuginfo packages by 200x Frank Ch. Eigler
2024-07-16 21:15 ` Omar Sandoval
2024-07-16 22:15 ` Frank Ch. Eigler
2024-07-16 22:17 ` Omar Sandoval
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox