Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8fdaa6ea authored by T.J. Mercier's avatar T.J. Mercier
Browse files

Don't restart procfs reads from scratch

android_os_Process_readProcFile is mainly used for small files. It
tries to minimize memory use by first attempting to read into a stack
buffer, and only after that buffer is found to be too small does it use
a heap-allocated buffer. The heap buffer starts out small and grows as
the file size is found to exceed the size of the buffer. However on both
the transition from the stack buffer to the heap buffer, and when
enlarging the heap buffer, the data that has already been read gets
thrown away and the read of the file restarts from the beginning.

The dominating runtime cost for the function is the kernel work to
produce the output. Copying the already-read data into a larger buffer
in userspace is more efficient than throwing it away and rereading from
the beginning. So this commit changes the function to do that.

This reduces cpu-cycles by about 35% for reads of the largest
/proc/pid/maps file on Android. Tested on a Pixel 6 Pro with
system_server's maps file across 20 runs:

         Simpleperf event count (cpu-cycles)
         Copy         Reread
Average  13,338,222   20,254,826
StdDev    4,791,640    9,644,942
Min       7,460,801   12,576,877
Max      25,403,534   45,759,654

Eliminating the 1024 byte stack buffer and just using an expandable heap
buffer indicates further improvement with an average of 12,835,802
cycles, code simplification, and a .text savings of between about 50-200
bytes depending on compiler optimizations. I suspect the memory savings
from attempting to read small files into the stack buffer is not that
meaningful, however I didn't measure peak memory use for either small or
large reads so I did not remove it in this commit.

Bug: 351917521
Test: cuttlefish boot
Change-Id: Ifa4a739ca43415b1d8dd85177a7c50eb86955160
parent 016bd093
Loading
Loading
Loading
Loading
+38 −46
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@

#include <algorithm>
#include <array>
#include <cstring>
#include <limits>
#include <memory>
#include <string>
@@ -50,7 +51,6 @@
#include <inttypes.h>
#include <pwd.h>
#include <signal.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/errno.h>
#include <sys/pidfd.h>
@@ -73,13 +73,13 @@ static constexpr bool kDebugProc = false;
// readProcFile() are reading files under this threshold, e.g.,
// /proc/pid/stat.  /proc/pid/time_in_state ends up being about 520
// bytes, so use 1024 for the stack to provide a bit of slack.
static constexpr ssize_t kProcReadStackBufferSize = 1024;
static constexpr size_t kProcReadStackBufferSize = 1024;

// The other files we read from proc tend to be a bit larger (e.g.,
// /proc/stat is about 3kB), so once we exhaust the stack buffer,
// retry with a relatively large heap-allocated buffer.  We double
// this size and retry until the whole file fits.
static constexpr ssize_t kProcReadMinHeapBufferSize = 4096;
static constexpr size_t kProcReadMinHeapBufferSize = 4096;

#if GUARD_THREAD_PRIORITY
Mutex gKeyCreateMutex;
@@ -1064,61 +1064,53 @@ jboolean android_os_Process_readProcFile(JNIEnv* env, jobject clazz,
    }

    // Most proc files we read are small, so we go through the loop
    // with the stack buffer firstly. We allocate a buffer big
    // enough for the whole file.

    char readBufferStack[kProcReadStackBufferSize];
    std::unique_ptr<char[]> readBufferHeap;
    char* readBuffer = &readBufferStack[0];
    ssize_t readBufferSize = kProcReadStackBufferSize;
    ssize_t numberBytesRead;
    // with the stack buffer first. We allocate a buffer big enough
    // for most files.

    char stackBuf[kProcReadStackBufferSize];
    std::vector<char> heapBuf;
    char* buf = stackBuf;

    size_t remaining = sizeof(stackBuf);
    off_t offset = 0;
    for (;;) {
        ssize_t requestedBufferSize = readBufferSize - offset;
        // By using pread, we can avoid an lseek to rewind the FD
        // before retry, saving a system call.
        numberBytesRead =
                TEMP_FAILURE_RETRY(pread(fd, readBuffer + offset, requestedBufferSize, offset));
        if (numberBytesRead < 0) {
    ssize_t numBytesRead;

    do {
        numBytesRead = TEMP_FAILURE_RETRY(pread(fd, buf + offset, remaining, offset));
        if (numBytesRead < 0) {
            if (kDebugProc) {
                ALOGW("Unable to read process file err: %s file: %s fd=%d\n",
                      strerror_r(errno, &readBufferStack[0], sizeof(readBufferStack)), file8.get(),
                      fd.get());
                      strerror_r(errno, stackBuf, sizeof(stackBuf)), file8.get(), fd.get());
            }
            return JNI_FALSE;
        }
        if (numberBytesRead == 0) {
            // End of file.
            numberBytesRead = offset;
            break;
        }
        if (numberBytesRead < requestedBufferSize) {
            // Read less bytes than requested, it's not an error per pread(2).
            offset += numberBytesRead;

        offset += numBytesRead;
        remaining -= numBytesRead;

        if (numBytesRead && !remaining) {
            if (buf == stackBuf) {
                heapBuf.resize(kProcReadMinHeapBufferSize);
                static_assert(kProcReadMinHeapBufferSize > sizeof(stackBuf));
                std::memcpy(heapBuf.data(), stackBuf, sizeof(stackBuf));
            } else {
            // Buffer is fully used, try to grow it.
            if (readBufferSize > std::numeric_limits<ssize_t>::max() / 2) {
                if (heapBuf.size() >= std::numeric_limits<ssize_t>::max() / 2) {
                    if (kDebugProc) {
                    ALOGW("Proc file too big: %s fd=%d\n", file8.get(), fd.get());
                        ALOGW("Proc file too big: %s fd=%d size=%zu\n",
                              file8.get(), fd.get(), heapBuf.size());
                    }
                    return JNI_FALSE;
                }
            readBufferSize = std::max(readBufferSize * 2, kProcReadMinHeapBufferSize);
            readBufferHeap.reset(); // Free address space before getting more.
            readBufferHeap = std::make_unique<char[]>(readBufferSize);
            if (!readBufferHeap) {
                jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
                return JNI_FALSE;
            }
            readBuffer = readBufferHeap.get();
            offset = 0;
                heapBuf.resize(2 * heapBuf.size());
            }
            buf = heapBuf.data();
            remaining = heapBuf.size() - offset;
        }
    } while (numBytesRead != 0);

    // parseProcLineArray below modifies the buffer while parsing!
    return android_os_Process_parseProcLineArray(
        env, clazz, readBuffer, 0, numberBytesRead,
        format, outStrings, outLongs, outFloats);
        env, clazz, buf, 0, offset, format, outStrings, outLongs, outFloats);
}

void android_os_Process_setApplicationObject(JNIEnv* env, jobject clazz,