การเพิ่มพื้นที่ว่างให้กับออบเจ็กต์ในโค้ดของแอปหรือการปิดสตรีมชั่วคราวไม่ได้ทำให้ RAM จริงกลับไปที่ระบบปฏิบัติการเสมอไป ตัวจัดสรรเนทีฟที่กำหนดเอง รันไทม์ที่รวบรวมขยะ และตารางแฮชแบบเปิดมักจะเก็บอารีนาหน่วยความจำเสมือนที่ยังคงได้รับการสนับสนุนโดยหน้าจริงเป็นเวลานานหลังจากที่ข้อมูลเชิงตรรกะภายในหายไป ในทำนองเดียวกัน การอ่านหรือการแมปหน่วยความจำของที่เก็บถาวรเนื้อหาขนาดใหญ่จะสร้างแคชหน้าเคอร์เนล ซึ่งยังคงเรียกเก็บเงินจากกลุ่มควบคุมหน่วยความจำ (memcg) ของแอป
หน้านี้จะอธิบายวิธีใช้การเรียกใช้ระบบ madvise และ posix_fadvise เพื่อแสดงหน้าเว็บที่ไม่ระบุตัวตนและหน้าเว็บที่สำรองไฟล์ซึ่งไม่ได้ใช้ไปยังระบบปฏิบัติการ โดยยังคงรักษาการจองที่อยู่เสมือนและตัวบอกไฟล์ไว้เหมือนเดิม รวมถึงวิธีตรวจสอบผลกระทบใน adb shell และ Perfetto
เหตุใดการจัดสรรคืนเชิงตรรกะจึงยังคงใช้ RAM จริง
เคอร์เนล Linux จะติดตามหน่วยความจำกายภาพที่ระดับความละเอียดของหน้าฮาร์ดแวร์ ซึ่งมีขนาด 4 KB หรือ 16 KB ในอุปกรณ์ Android เมื่อกระบวนการจองพื้นที่ของพื้นที่ที่อยู่เสมือนด้วย mmap เคอร์เนลจะสร้างพื้นที่หน่วยความจำเสมือน (VMA) โดยไม่จัดสรร RAM จริง เคอร์เนลจะจัดสรรและเรียกเก็บเงินหน้าเว็บจริงก็ต่อเมื่อกระบวนการเข้าถึงหน้าเว็บนั้นเป็นครั้งแรกเท่านั้น สำหรับหน่วยความจำแบบไม่ระบุชื่อ การอ่านครั้งแรกจะแมปหน้าศูนย์ที่แชร์ ดังนั้นการเขียนเท่านั้นที่จะจัดสรร RAM
เมื่อหน้าเว็บเกิดข้อผิดพลาดใน RAM เคอร์เนลจะไม่ทราบว่าไบต์ภายในหน้าเว็บนั้นยังคงแสดงออบเจ็กต์ที่ทำงานอยู่ในรันไทม์ของแอปหรือไม่ รูปแบบที่พบบ่อย 2 รูปแบบที่ทำให้หน้าจริงยังคงอยู่หลังจากที่แอปใช้งานเสร็จแล้ว ได้แก่
- อารีนาและพูลที่ไม่ระบุชื่อ: ตัวจัดสรรเนทีฟที่กำหนดเอง (เช่น ตัวจัดสรร Slab, พูล หรือเฟรมอารีนา) รันไทม์ที่มีการจัดการซึ่งมีพื้นที่ฮีปที่ไม่เคลื่อนที่ และตารางแฮชขนาดใหญ่จะจองช่วงที่อยู่เสมือนที่อยู่ติดกันด้วย
mmapโดยใช้MAP_PRIVATE | MAP_ANONYMOUSเมื่อแอปรีเซ็ตอารีนา ปล่อยบล็อกกลับไปยังพูลที่กำหนดเอง หรือล้างตารางแฮชโดยการตั้งค่าข้อมูลเมตาเป็น 0 หน้าที่เกี่ยวข้องจะยังคงเป็นหน้าสกปรก หากไม่มีคำแนะนำที่ชัดเจนสำหรับเคอร์เนล หน้าเหล่านั้นจะยังคงนับรวมใน Resident Set Size (RSS) และ Proportional Set Size (PSS) แบบไม่ระบุตัวตนของกระบวนการ และการเรียกเก็บเงิน cgroupmemory.currentของกระบวนการ ภายใต้แรงกดดันด้านหน่วยความจำ เคอร์เนลจะบีบอัดหน้าหน่วยความจำเหล่านั้นเป็น ZRAM ซึ่งยังคงใช้พื้นที่ Swap ที่เรียกเก็บจากmemory.swap.current - แคชหน้าเว็บที่สำรองข้อมูลด้วยไฟล์จากการสตรีมเนื้อหา: เมื่อแอปอ่าน Asset Bundle ขนาดใหญ่, ไฟล์เก็บถาวรระดับ, คอนเทนเนอร์สื่อ หรือโมเดลแมชชีนเลิร์นนิงโดยใช้
read,preadหรือmmapที่สำรองข้อมูลด้วยไฟล์ เคอร์เนลจะแคชหน้าเว็บที่อ่านทุกหน้าในแคชหน้าเว็บ และเรียกเก็บเงินหน้าเว็บเหล่านั้นจากตัวนับfileในไฟล์memory.statของ cgroup ของกระบวนการ หากแอปคลายการบีบอัดหรือคัดลอกเนื้อหานั้นลงในบัฟเฟอร์รันไทม์ที่ไม่ระบุชื่อหรืออัปโหลดไปยัง GPU การเก็บหน้าไฟล์ข้อมูลดิบไว้ในแคชหน้าเว็บจะทำซ้ำเนื้อหาใน RAM
เผยแพร่หน้าเว็บที่ไม่ระบุชื่อ
การเรียกใช้ระบบ madvise จะให้คำแนะนำแก่เคอร์เนลเกี่ยวกับวิธีที่กระบวนการของคุณคาดว่าจะใช้ช่วงที่อยู่เสมือน สำหรับการแมปแบบไม่ระบุตัวตนส่วนตัว (MAP_PRIVATE |
MAP_ANONYMOUS) การส่ง MADV_DONTNEED จะสั่งให้เคอร์เนลทิ้งหน้าจริงและรายการ ZRAM ที่สลับกันซึ่งสนับสนุนช่วงที่จัดแนวหน้าเว็บที่ระบุทันที
หลังจาก madvise โดยมี MADV_DONTNEED แสดงผลดังนี้
- ช่วงที่อยู่เสมือนยังคงสงวนไว้และใช้ได้ในพื้นที่ที่อยู่ของกระบวนการ (
VmSizeไม่เปลี่ยนแปลง) ดังนั้นพอยน์เตอร์และขอบเขตอารีนาจึงยังคงอยู่โดยไม่ต้องเรียกใช้munmapหรือmmap - ระบบจะยกเลิกการแมปและปล่อยหน้าจริงทันที ซึ่งจะลด
RssAnon,Pss, ตัวนับanonในmemory.statและmemory.swap.currentสำหรับ หน้าใดก็ตามที่อยู่ใน ZRAM - การอ่านหรือเขียนที่อยู่ใดๆ ในช่วงนั้นในภายหลังจะทำให้เกิดข้อผิดพลาดในหน้าเว็บที่เติมศูนย์ใหม่ตามต้องการ
| แฟล็กคำแนะนำ | การแมปเป้าหมาย | ลักษณะการทำงานของเคอร์เนลใน Android |
|---|---|---|
MADV_DONTNEED |
ไม่ระบุตัวตนแบบส่วนตัว (MAP_PRIVATE | MAP_ANONYMOUS) |
ทิ้งหน้าจริงและช่องสลับ ZRAM ทันที การเข้าถึงในภายหลังจะทำให้เกิดข้อผิดพลาดในหน้าเว็บที่ไม่มีข้อมูล การลดลงเชิงกำหนดใน RssAnon และ memory.current |
MADV_DONTNEED |
ไฟล์ที่สำรองข้อมูล (MAP_PRIVATE หรือ MAP_SHARED) |
ยกเลิกการแมปหน้าเว็บจากตารางหน้าเว็บของกระบวนการ (RssFile
การยกเลิก) ระบบจะทิ้งหน้า Copy-on-write ส่วนตัวในการแมป MAP_PRIVATE ดังนั้นการแก้ไขจะสูญหายและการเข้าถึงครั้งถัดไปจะอ่านไฟล์อีกครั้ง ระบบจะเขียนหน้า MAP_SHARED ที่มีการแก้ไขกลับไปยังพื้นที่เก็บข้อมูลในภายหลัง หน้าเว็บที่ไม่มีการแก้ไขจะยังคงอยู่ในแคชของหน้าเว็บจนกว่าจะถูกนำออก |
MADV_FREE |
ไม่ระบุตัวตนแบบส่วนตัว (MAP_PRIVATE | MAP_ANONYMOUS) |
ทำเครื่องหมายหน้าเว็บว่านำมาใช้ซ้ำแบบ Lazy ได้ เคอร์เนลจะเก็บหน้าไว้ใน RssAnon หากไม่มีแรงกดดันด้านหน่วยความจำ และจะเรียกคืนหน้าเหล่านั้นก็ต่อเมื่อมีการเรียกคืนส่วนกลางหรือ memcg เท่านั้น |
MADV_WILLNEED |
ไฟล์ที่สำรองข้อมูลหรือสลับแบบไม่ระบุตัวตน | เริ่มการอ่านล่วงหน้าแบบอะซิงโครนัสสำหรับการแมปที่สำรองข้อมูลไฟล์ หรืออ่านหน้าเว็บที่ไม่ระบุตัวตนที่สลับกลับจาก ZRAM ไปยังแคชสลับล่วงหน้า |
จัดแนวช่วงภายในให้สอดคล้องกับขนาดหน้าของรันไทม์
madvise ทำงานกับทั้งหน้าเว็บ โดยต้องใช้ที่อยู่เริ่มต้นที่สอดคล้องกับหน้าและปัดเศษความยาวขึ้นเป็นขอบเขตหน้าถัดไป หากคุณปล่อยช่วงภายในอารีนาที่ใช้ร่วมกันซึ่งไบต์ที่อยู่ใกล้เคียงยังคงมีออบเจ็กต์ที่ใช้งานอยู่ การปัดเศษช่วงออกด้านนอกอาจทำให้การจัดสรรที่ใช้งานอยู่ซึ่งใช้หน้าขอบเขตร่วมกันเป็นศูนย์
ค้นหาขนาดหน้าหน่วยความจำรันไทม์ด้วย sysconf(_SC_PAGESIZE) เสมอเพื่อรองรับทั้งอุปกรณ์ที่มีหน้าหน่วยความจำขนาด 4 KB และ 16 KB และปัดเศษที่อยู่เริ่มต้นภายในขึ้นไปยังขอบเขตหน้าถัดไป และปัดเศษที่อยู่สิ้นสุดภายในลงไปยังขอบเขตหน้าก่อนหน้า ดูคำแนะนำเพิ่มเติมเกี่ยวกับตัวจัดสรรที่ไม่ขึ้นอยู่กับขนาดหน้าได้ที่เพิ่มประสิทธิภาพตัวจัดสรรหน่วยความจำที่กำหนดเอง
#include <sys/mman.h>
#include <unistd.h>
#include <cstdint>
// Reclaims physical pages strictly inside [region_start, region_start + length)
// without disturbing live objects on partially occupied boundary pages.
void ReleaseUnusedArenaSpan(void* region_start, size_t length) {
const uintptr_t page_size = static_cast<uintptr_t>(sysconf(_SC_PAGESIZE));
const uintptr_t page_mask = ~(page_size - 1);
const uintptr_t raw_start = reinterpret_cast<uintptr_t>(region_start);
const uintptr_t raw_end = raw_start + length;
// Round start UP and end DOWN so only fully unused pages are discarded.
const uintptr_t aligned_start = (raw_start + page_size - 1) & page_mask;
const uintptr_t aligned_end = raw_end & page_mask;
if (aligned_end > aligned_start) {
madvise(reinterpret_cast<void*>(aligned_start),
aligned_end - aligned_start,
MADV_DONTNEED);
}
}
เมื่อสร้างการแมปแบบไม่ระบุตัวตนที่กำหนดเองด้วย mmap ให้ติดป้ายกำกับ VMA ด้วย prctl โดยใช้ PR_SET_VMA_ANON_NAME เพื่อให้คุณติดตามตำแหน่งที่อยู่ตามชื่อใน showmap และ /proc/<pid>/smaps ได้ dumpsys
meminfo นับขอบเขตที่ไม่ระบุชื่อซึ่งตั้งชื่อเองใน Unknown
เคอร์เนลรุ่นเก่าบางรุ่นจะเก็บตัวชี้ไปยังชื่อแทนที่จะคัดลอกชื่อ ดังนั้นให้ส่งสตริงที่ยังคงใช้ได้ตลอดอายุการใช้งานของการแมป เช่น สตริงลิเทอรัล
#include <sys/mman.h>
#include <sys/prctl.h>
#ifndef PR_SET_VMA
#define PR_SET_VMA 0x53564d41
#define PR_SET_VMA_ANON_NAME 0
#endif
void* ReserveNamedArena(size_t capacity_bytes, const char* arena_name) {
void* ptr = mmap(nullptr, capacity_bytes, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr != MAP_FAILED) {
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME,
reinterpret_cast<unsigned long>(ptr), capacity_bytes,
reinterpret_cast<unsigned long>(arena_name));
}
return ptr;
}
ลบหน้าแคชของไฟล์
เมื่อแอปอ่านข้อมูลจากตัวบอกไฟล์ เคอร์เนลจะแคชหน้าของไฟล์ในแคชหน้าเพื่อเตรียมพร้อมสำหรับการอ่านในอนาคต แม้ว่าวิธีนี้จะช่วยเร่งการเข้าถึงไฟล์การกำหนดค่าขนาดเล็กหรือคลังที่ใช้ร่วมกันซ้ำๆ แต่ก็อาจส่งผลเสียเมื่อสตรีมที่เก็บถาวรของเนื้อหาขนาดใหญ่ แพ็กเกจพื้นผิว หรือกลุ่มเสียงและวิดีโอที่แยกวิเคราะห์เพียงครั้งเดียวและจัดเก็บในรูปแบบที่ถอดรหัสแล้วที่อื่น
การเรียกใช้ระบบ posix_fadvise ช่วยให้แอปแจ้งเคอร์เนลเกี่ยวกับรูปแบบการเข้าถึงไฟล์ที่คาดไว้ในตัวบอกไฟล์ที่เปิดอยู่ได้
POSIX_FADV_DONTNEED: สั่งให้เคอร์เนลทราบว่าจะไม่มีการเข้าถึงช่วงไบต์ที่ระบุในอนาคตอันใกล้ เคอร์เนลจะล้างหน้าแคชที่สะอาดและไม่ได้แมปในช่วงที่จัดหน้าจากแคชหน้าทันที ซึ่งจะลดตัวนับfileในmemory.statและการเรียกเก็บเงินmemory.currentของ cgroupPOSIX_FADV_SEQUENTIAL: เพิ่มหน้าต่างการอ่านล่วงหน้าของเคอร์เนลเป็น 2 เท่าสำหรับตัวบอกไฟล์ ซึ่งจะช่วยปรับปรุงอัตราการส่งผ่านข้อมูลเมื่อสตรีมไฟล์ที่เก็บถาวรขนาดใหญ่ตั้งแต่ต้นจนจบ ใช้ร่วมกับPOSIX_FADV_DONTNEEDในส่วนท้ายขณะที่ใช้ เพื่อให้รอยเท้าของแคชหน้าเว็บยังคงจำกัดอยู่ในหน้าต่างแบบเลื่อนขนาดเล็กPOSIX_FADV_RANDOM: ปิดใช้การอ่านล่วงหน้าของเคอร์เนลเชิงคาดการณ์ในตัวบอกไฟล์ ใช้ตัวเลือกนี้เมื่อทำการค้นหาแบบกระจัดกระจายและไม่ต่อเนื่องตามออฟเซ็ตไบต์ในไฟล์ที่เก็บถาวรหรือไฟล์ฐานข้อมูลขนาดใหญ่ เพื่อให้การอ่านขนาดเล็กแต่ละครั้งไม่ดึงหน้าเว็บที่ไม่ได้ใช้ซึ่งอยู่ติดกันเข้าไปในแคชของหน้าเว็บ สำหรับไฟล์ที่แมปหน่วยความจำ ให้เรียกใช้madviseโดยใช้MADV_RANDOMในการแมปแทน
ตัวอย่างต่อไปนี้จะสตรีมที่เก็บถาวรของชิ้นงานขนาดใหญ่เป็นกลุ่มๆ ซึ่งช่วยให้สามารถอ่านล่วงหน้าตามลำดับล่วงหน้าและนำช่วงที่จัดแนวหน้าเว็บที่ประมวลผลแล้วแต่ละช่วงออกจากแคชของหน้าเว็บได้
#include <fcntl.h>
#include <unistd.h>
#include <cstdint>
#include <vector>
void StreamAssetArchive(int fd, size_t total_size) {
const size_t page_size = static_cast<size_t>(sysconf(_SC_PAGESIZE));
constexpr size_t kChunkSize = 4 * 1024 * 1024; // 4 MB streaming window
std::vector<uint8_t> buffer(kChunkSize);
// Hint sequential access so the kernel prefetches upcoming chunks.
posix_fadvise(fd, 0, total_size, POSIX_FADV_SEQUENTIAL);
off_t offset = 0;
while (offset < static_cast<off_t>(total_size)) {
ssize_t bytes_read = pread(fd, buffer.data(), buffer.size(), offset);
if (bytes_read <= 0) break;
// Decode or upload buffer[0 .. bytes_read) into runtime structures here.
// Evict full pages that have been completely consumed.
const size_t aligned_len =
(static_cast<size_t>(bytes_read) / page_size) * page_size;
if (aligned_len > 0) {
posix_fadvise(fd, offset, static_cast<off_t>(aligned_len),
POSIX_FADV_DONTNEED);
}
offset += bytes_read;
}
}
เรียกคืนพื้นที่ mmap ที่มีไฟล์สำรอง
หากแอปของคุณแมปหน่วยความจำกับไฟล์เนื้อหาขนาดใหญ่ด้วย mmap แทนที่จะอ่านด้วย pread การเรียกใช้ posix_fadvise ด้วย POSIX_FADV_DONTNEED เพียงอย่างเดียวจะไม่ลบหน้าเว็บที่ยังคงแมปอยู่ในตารางหน้าเว็บของกระบวนการ วิธีวางช่วงที่ใช้ไปแล้วของการแมปไฟล์แบบอ่านอย่างเดียวโดยไม่ต้องยกเลิกการแมป VMA ทั้งหมด
- เรียก
madvise(addr, length, MADV_DONTNEED)ในช่วงที่อยู่เสมือนที่แมปไว้เพื่อยกเลิกการแมปหน้าเหล่านั้นจากตารางหน้าของกระบวนการ (RssFileลดลง) - เรียกใช้
posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED)ในออฟเซ็ตไฟล์ที่เกี่ยวข้องเพื่อให้เคอร์เนลลบหน้าสะอาดที่ไม่ได้แมปออกจากแคชหน้าด้วย (fileในmemory.statลดลง)
ปฏิบัติตามหลักเกณฑ์เกี่ยวกับคำแนะนำในหน้าเว็บ
ใช้แนวทางปฏิบัติเหล่านี้เมื่อผสานรวม madvise และ posix_fadvise เข้ากับ Allocator, ตัวโหลดเนื้อหา และตัวแฮนเดิลวงจร
- การเรียกใช้แบบกลุ่มในพื้นที่แบบหยาบ: การเรียกใช้
madviseแต่ละครั้งจะใช้การล็อกแผนที่หน่วยความจำของกระบวนการ (mmap_lock) เดินผ่านตารางหน้า และทำให้รายการ Translation Lookaside Buffer (TLB) ที่ตรงกันในแกน CPU อื่นๆ ไม่ถูกต้อง (พร้อมวิธีการบำรุงรักษา TLB แบบบรอดแคสต์ใน Arm และการขัดจังหวะระหว่างโปรเซสเซอร์ใน x86) อย่าเรียกใช้madviseทุกครั้งที่มีการจัดสรรคืนเล็กน้อย รวมช่วงที่ว่างและแจ้งเคอร์เนลเป็นกลุ่มขนาด 64 KB ถึง 1 MB หรือใหญ่กว่า หรือกวาดช่วงที่ว่างระหว่างขอบเขตเฟรมและการเปลี่ยนพื้นหลัง (onStopและTRIM_MEMORY_UI_HIDDEN) - เรียกคืนความจุที่ว่างเปล่าในตารางแฮชและพูล: เมื่อตารางแฮชขนาดใหญ่ พูลออบเจ็กต์ หรือบัฟเฟอร์การทดลองใช้ถูกล้างหลังจากเปลี่ยนฉากหรือซิงค์ข้อมูล ให้ลดการจัดสรรสำรองหรือออก
MADV_DONTNEEDในอาร์เรย์สำรองที่ล้างแล้ว การเก็บตารางที่อยู่แบบเปิดขนาด 32 MB ที่จัดสรรด้วย Tombstone จะทำให้หน้าจริงขนาด 32 MB ทั้งหมดถูกเรียกเก็บเงินจากกระบวนการของคุณ - ล้างข้อมูลไฟล์ที่สกปรกก่อนที่จะขับออก:
POSIX_FADV_DONTNEEDจะขับเฉพาะหน้าแคชของหน้าที่สะอาดออกเท่านั้น หากแอปเขียนไฟล์แคชหรือไฟล์ชั่วคราวขนาดใหญ่ที่ไม่ได้อ่านกลับทันที ให้เรียกใช้fdatasyncก่อนposix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)เพื่อให้ระบบเขียนหน้าเว็บที่สกปรกลงในพื้นที่เก็บข้อมูลแล้วลบออกจาก RAM - อย่าขับไล่โค้ดหรือชิ้นงานที่ใช้ร่วมกันซึ่งมีการใช้งานบ่อย: หลีกเลี่ยงการเรียกใช้
POSIX_FADV_DONTNEEDใน DEX, ODEX, VDEX, คลังที่ใช้ร่วมกัน (.so) หรือชิ้นงาน UI ที่มีการเข้าถึงซ้ำๆ การลบโค้ดหรือหน้าชิ้นงานที่ใช้งานอยู่จะบังคับให้เกิดข้อผิดพลาดในการจัดเก็บข้อมูลแบบซิงโครนัสซ้ำ ซึ่งจะทำให้เธรดหยุดทำงานและเพิ่มแรงกดดันด้าน I/O ตามที่รายงานโดยข้อมูลการหยุดทำงานเนื่องจากแรงกดดัน (PSI)
ยืนยันการเรียกคืนหน้าเว็บใน Shell
ตรวจสอบว่า madvise และ posix_fadvise เรียกคืน RAM จริงโดยใช้ showmap และไฟล์การบันทึกการใช้งาน cgroup v2 ของแอป
ตรวจสอบขนาดเสมือนเทียบกับขนาดจริงด้วย showmap
เมื่อคุณตั้งชื่ออารีนาที่ไม่ระบุชื่อด้วย PR_SET_VMA_ANON_NAME, showmap จะแสดงทั้งการจองเสมือน (virtual size) และหน้าจริงที่อยู่ (RSS และ PSS) เอาต์พุตตัวอย่างต่อไปนี้มาจากแอปตัวอย่าง MemoryLab ใน AOSP ซึ่งตั้งชื่ออารีนาว่า memorylab-arena แทนที่ com.example.app และ memorylab-arena ด้วยชื่อแพ็กเกจและชื่ออารีนาของคุณ ก่อนเรียกใช้
madvise ด้วย MADV_DONTNEED ในอารีนาขนาด 64 MB ที่มีการแตะ ให้เรียกใช้คำสั่งต่อไปนี้
adb shell "showmap \$(pidof com.example.app) | grep -E '^ +virtual|^ +size|memorylab-arena' | head -3"
virtual shared shared private private Anon Shmem File Shared Private
size RSS PSS clean dirty clean dirty swap swapPSS HugePages PmdMapped PmdMapped Hugetlb Hugetlb Locked # object
65536 65536 65536 0 0 0 65536 0 0 0 0 0 0 0 0 1 [anon:memorylab-arena]
หลังจากเรียกใช้ madvise(arena_ptr, 64 * 1024 * 1024, MADV_DONTNEED) แล้ว ให้เรียกใช้ showmap อีกครั้ง ดังนี้
virtual shared shared private private Anon Shmem File Shared Private
size RSS PSS clean dirty clean dirty swap swapPSS HugePages PmdMapped PmdMapped Hugetlb Hugetlb Locked # object
65536 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 [anon:memorylab-arena]
virtual size จะยังคงอยู่ที่ 65536 KB (64 MB) ซึ่งยืนยันว่าช่วงที่อยู่เสมือนยังคงสงวนไว้ ขณะที่ RSS, PSS และ private dirty ลดลงเหลือ 0 KB
ตรวจสอบ memory.current และ memory.stat ของ cgroup
หากต้องการยืนยันทั้งการเรียกคืนหน้าเว็บที่ไม่ระบุตัวตนและการขับไล่แคชของหน้าที่ไม่ได้แมป (ซึ่งไม่ปรากฏในตารางหน้าของกระบวนการหากอ่านด้วย pread) ให้ตรวจสอบไดเรกทอรี cgroup v2 ของกระบวนการ ใน Android กระบวนการของแต่ละแอปจะมี cgroup ของตัวเอง (เช่น /sys/fs/cgroup/apps/uid_<UID>/pid_<PID>/ ขึ้นอยู่กับอุปกรณ์)
คำสั่งต่อไปนี้จะค้นหาไดเรกทอรีจากบรรทัด 0:: ของ /proc/<PID>/cgroup และพิมพ์ตัวนับ
adb shell '
PID=$(pidof com.example.app)
CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
echo "memory.current: $(cat $CG/memory.current)"
echo "memory.swap.current: $(cat $CG/memory.swap.current)"
grep -E "^(anon|file) " $CG/memory.stat
'
ก่อนที่จะปล่อยอารีนาแบบไม่ระบุชื่อขนาด 64 MB และนำไฟล์เนื้อหาที่แมปหน่วยความจำขนาด 128 MB ออกจากแคชของหน้าเว็บ ให้ทำดังนี้
memory.current: 242962432
memory.swap.current: 16633856
anon 100671488
file 140009472
ทันทีหลังจากเรียก madvise ด้วย MADV_DONTNEED ในอารีนาและการแมปไฟล์เนื้อหา จากนั้นเรียก posix_fadvise ด้วย POSIX_FADV_DONTNEED ในตัวบอกไฟล์เนื้อหา
memory.current: 41766912
memory.swap.current: 16502784
anon 33722368
file 5914624
anon ลดลง 64 MB และ file ลดลง 128 MB ซึ่งจะลด memory.current ลงประมาณ 192 MB โดยไม่ต้องปิดตัวบอกไฟล์หรือยกเลิกการแมปพื้นที่ที่อยู่เสมือนของอารีนา memory.swap.current แทบไม่มีการเปลี่ยนแปลง เนื่องจากแทบไม่มีหน้าอารีนาที่สลับไปยัง ZRAM ค่าของคุณจะขึ้นอยู่กับอุปกรณ์และจำนวนไฟล์ที่เคอร์เนลถูกขับออกไปเองแล้ว ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีที่เคอร์เนลเรียกเก็บเงินและเรียกคืนหน่วยความจำต่อ cgroup ได้ที่การเรียกคืนหน่วยความจำ: การขับออกและการสลับ
วิเคราะห์การเรียกคืนหน้าเว็บใน Perfetto
Perfetto ช่วยให้คุณเชื่อมโยงการเรียก madvise และ posix_fadvise กับตัวนับตารางหน้าและแคชหน้าของเคอร์เนลในไทม์ไลน์แบบรวมได้
บันทึกการติดตาม
บันทึกการติดตามด้วย linux.process_stats สำหรับตัวนับกระบวนการเป็นระยะและ linux.ftrace สำหรับเหตุการณ์ RSS และแคชหน้าเว็บแบบเรียลไทม์ จากนั้นดึงข้อมูลไปยังคอมพิวเตอร์ของคุณ
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/page_advice.perfetto-trace <<EOF
buffers: {
size_kb: 65536
fill_policy: RING_BUFFER
}
data_sources: {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
proc_stats_poll_ms: 100
}
}
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "kmem/rss_stat"
ftrace_events: "filemap/mm_filemap_add_to_page_cache"
ftrace_events: "filemap/mm_filemap_delete_from_page_cache"
}
}
}
duration_ms: 20000
EOF
adb pull /data/misc/perfetto-traces/page_advice.perfetto-trace
ตรวจสอบแทร็กใน UI ของ Perfetto
เปิดการติดตามใน Perfetto UI ขยายกลุ่มกระบวนการของแอป และปักหมุดแทร็กตัวนับต่อไปนี้เพื่อให้อยู่ที่ด้านบนของไทม์ไลน์
mem.virt: ยังคงคงที่ในการเรียกMADV_DONTNEEDครั้ง ซึ่งยืนยันว่าการจองที่อยู่เสมือน (mmap) ไม่มีการเปลี่ยนแปลงmem.rss.anon: ลดลงอย่างรวดเร็วที่การประทับเวลาที่แน่นอนซึ่งแอปของคุณเรียกใช้madviseด้วยMADV_DONTNEEDในอารีนาที่ไม่ระบุตัวตนหรือพื้นที่ฮีปที่สกปรกmem.rss.file: ลดระดับลงเมื่อแอปของคุณเรียกใช้madviseด้วยMADV_DONTNEEDในพื้นที่mmapที่สำรองข้อมูลไว้ในไฟล์ พร้อมกับเหตุการณ์ ftracemm_filemap_delete_from_page_cacheที่เกิดขึ้นอย่างรวดเร็วเมื่อposix_fadviseด้วยPOSIX_FADV_DONTNEEDนำหน้าเว็บที่สะอาดออกจากแคชของหน้าเว็บmem.swap: ลดลงเมื่อMADV_DONTNEEDทิ้งหน้าเว็บที่ไม่ระบุตัวตนซึ่งก่อนหน้านี้บีบอัดเป็น ZRAM
รูปที่ 2 แสดงแทร็กเหล่านี้สำหรับแอปตัวอย่าง MemoryLab หากต้องการสำรวจมุมมองเดียวกัน ให้เปิดการติดตามตัวอย่างใน Perfetto UI
ค้นหาหน่วยความจำสูงสุดและหน่วยความจำสุดท้ายด้วย PerfettoSQL
เรียกใช้การค้นหา PerfettoSQL ต่อไปนี้ใน Perfetto UI (แท็บการค้นหา (SQL)) เพื่อเปรียบเทียบค่าสูงสุดและค่าสุดท้ายของ mem.virt, mem.rss.anon, mem.rss.file และ mem.swap สำหรับกระบวนการเป้าหมาย
WITH counters AS (
SELECT t.name AS counter_name, c.ts, c.value
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name = 'com.example.app'
AND t.name IN ('mem.virt', 'mem.rss.anon', 'mem.rss.file', 'mem.swap')
)
SELECT
counter_name,
CAST(MAX(value) / 1048576 AS INT) AS peak_mb,
CAST((
SELECT value FROM counters AS last
WHERE last.counter_name = counters.counter_name
ORDER BY ts DESC LIMIT 1
) / 1048576 AS INT) AS final_mb
FROM counters
GROUP BY counter_name
ORDER BY counter_name;
ตัวอย่างเอาต์พุตสำหรับการติดตามในรูปที่ 2
counter_name peak_mb final_mb
mem.rss.anon 111 48
mem.rss.file 212 84
mem.swap 36 36
mem.virt 13952 13952
mem.rss.anon ลดลง 63 MB และ mem.rss.file ลดลง 128 MB จากจุดสูงสุด ส่วน mem.virt สิ้นสุดที่จุดสูงสุดเนื่องจากการจองเสมือนไม่มีการเปลี่ยนแปลง
แก้ไขหน้าเว็บที่ถูกนำออกไปยังไฟล์
เหตุการณ์ mm_filemap_add_to_page_cache และ mm_filemap_delete_from_page_cache จะระบุไฟล์ตามหมายเลข inode (i_ino) แทนที่จะเป็นเส้นทาง หากต้องการยืนยันว่าหน้าที่ถูกลบเป็นของไฟล์ที่คุณแนะนำ ให้นับการลบต่อ inode ดังนี้
SELECT
EXTRACT_ARG(arg_set_id, 'i_ino') AS inode,
COUNT(*) AS evicted_pages
FROM ftrace_event
WHERE name = 'mm_filemap_delete_from_page_cache'
GROUP BY inode
ORDER BY evicted_pages DESC
LIMIT 5;
ตัวอย่างเอาต์พุตสำหรับการติดตามในรูปที่ 2
inode evicted_pages
9761 35781
76792192 767
126 668
0 612
11587712 514
ขณะที่อุปกรณ์ที่บันทึกการติดตามยังคงเชื่อมต่ออยู่ ให้แก้ไข inode แต่ละรายการเป็นเส้นทาง หมายเลข Inode จะไม่ซ้ำกันภายในระบบไฟล์เดียวเท่านั้น ดังนั้นให้จำกัดการค้นหาไว้ที่ระบบไฟล์ที่มีข้อมูลของแอป ดังนี้
INODE=<inode from the query>
adb shell "find /data -xdev -inum $INODE 2>/dev/null"
ตัวอย่างเอาต์พุตสำหรับ inode 9761:
/data/user/0/com.android.memorylab/files/asset_pack.bin
/data/data/com.android.memorylab/files/asset_pack.bin
ทั้ง 2 เส้นทางอ้างอิงถึงไฟล์เดียวกัน เนื่องจาก /data/user/0 และ /data/data เป็นชื่อ 2 ชื่อสำหรับไดเรกทอรีข้อมูลแอปเดียวกัน จำนวนการขับออกสำหรับไฟล์เนื้อหา (35,781 หน้า) มีขนาดใหญ่กว่าตัวไฟล์เอง (32,768 หน้าที่ 4 KB) เนื่องจาก kswapd ได้ขับส่วนหนึ่งของไฟล์ออกไปแล้วภายใต้แรงกดดันด้านหน่วยความจำก่อนที่แอปจะอ่านไฟล์กลับมาและเรียกใช้ posix_fadvise
สำหรับสคริปต์ที่แก้ไข inode หลายรายการพร้อมกัน โปรดดูหัวข้อการแก้ไขข้อผิดพลาดใน inode แล้วจึงแก้ไขไฟล์
← การโต้ตอบระหว่าง kswapd และ lmkd | ↑ ขึ้น | กระบวนการเบื้องหลังแบบ Lean →