Pedoman penyimpanan dalam cache sisi klien Android API

Panggilan API Android biasanya melibatkan latensi dan komputasi yang signifikan per pemanggilan. Oleh karena itu, penyimpanan dalam cache sisi klien adalah pertimbangan penting dalam mendesain API yang bermanfaat, benar, dan berperforma tinggi.

Motivasi

API yang diekspos kepada developer aplikasi di Android SDK sering kali diimplementasikan sebagai kode klien di Framework Android yang melakukan panggilan IPC Binder ke layanan sistem dalam proses platform, yang tugasnya adalah melakukan beberapa komputasi dan menampilkan hasil ke klien. Latensi operasi ini biasanya didominasi oleh tiga faktor:

  • Overhead IPC: panggilan IPC dasar biasanya 10.000x latensi panggilan metode dalam proses dasar.
  • Pertentangan sisi server: pekerjaan yang dilakukan di layanan sistem sebagai respons terhadap permintaan klien mungkin tidak langsung dimulai, misalnya jika thread server sibuk menangani permintaan lain yang tiba lebih awal.
  • Komputasi sisi server: pekerjaan itu sendiri untuk menangani permintaan di server mungkin memerlukan pekerjaan yang signifikan.

Anda dapat menghilangkan ketiga faktor latensi ini dengan menerapkan cache di sisi klien, asalkan cache tersebut:

  • Benar: cache sisi klien tidak pernah menampilkan hasil yang akan berbeda dengan yang akan ditampilkan server.
  • Efektif: permintaan klien sering kali ditayangkan dari cache, misalnya cache memiliki rasio hit yang tinggi.
  • Efisien: cache sisi klien menggunakan resource sisi klien secara efisien, seperti dengan merepresentasikan data yang di-cache dengan cara yang ringkas dan dengan tidak menyimpan terlalu banyak hasil yang di-cache atau data yang sudah tidak berlaku di memori klien.

Pertimbangkan untuk meng-cache hasil server di klien

Jika klien sering membuat permintaan yang sama persis beberapa kali, dan nilai yang ditampilkan tidak berubah dari waktu ke waktu, Anda harus menerapkan cache di library klien yang diberi kunci oleh parameter permintaan.

Sebaiknya gunakan IpcDataCache dalam penerapan Anda:

public class BirthdayManager {
    private final IpcDataCache.QueryHandler<User, Birthday> mBirthdayQuery =
            new IpcDataCache.QueryHandler<User, Birthday>() {
                @Override
                public Birthday apply(User user) {
                    return mService.getBirthday(user);
                }
            };
    private static final int BDAY_CACHE_MAX = 8;  // Maximum birthdays to cache
    private static final String BDAY_API = "getUserBirthday";
    private final IpcDataCache<User, Birthday> mCache
            new IpcDataCache<User, Birthday>(
                BDAY_CACHE_MAX, MODULE_SYSTEM, BDAY_API,  BDAY_API, mBirthdayQuery);

    /** @hide **/
    @VisibleForTesting
    public static void clearCache() {
        IpcDataCache.invalidateCache(MODULE_SYSTEM, BDAY_API);
    }

    public Birthday getBirthday(User user) {
        return mCache.query(user);
    }
}

Untuk contoh lengkap, lihat android.app.admin.DevicePolicyManager.

IpcDataCache tersedia untuk semua kode sistem, termasuk modul utama. Ada juga PropertyInvalidatedCache yang hampir identik, tetapi hanya terlihat oleh framework. Pilih IpcDataCache jika memungkinkan.

Membatalkan validasi cache pada perubahan sisi server

Jika nilai yang ditampilkan dari server dapat berubah dari waktu ke waktu, terapkan callback untuk mengamati perubahan, dan daftarkan callback sehingga Anda dapat membatalkan validasi cache sisi klien.

Membatalkan validasi cache di antara kasus pengujian unit

Dalam rangkaian pengujian unit, Anda dapat menguji kode klien terhadap double pengujian, bukan server sebenarnya. Jika ya, pastikan untuk menghapus cache sisi klien di antara kasus pengujian. Hal ini untuk menjaga kasus pengujian tetap hermetis, dan mencegah satu kasus pengujian mengganggu kasus pengujian lainnya.

@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {

    @Before
    public void setUp() {
        BirthdayManager.clearCache();
    }

    @After
    public void tearDown() {
        BirthdayManager.clearCache();
    }

    ...
}

Saat menulis pengujian CTS yang menggunakan klien API yang menggunakan penyimpanan dalam cache secara internal, cache adalah detail implementasi yang tidak ditampilkan kepada penulis API, sehingga pengujian CTS tidak boleh memerlukan pengetahuan khusus tentang penyimpanan dalam cache yang digunakan dalam kode klien.

Mempelajari hit dan miss cache

IpcDataCache dan PropertyInvalidatedCache dapat mencetak statistik langsung:

adb shell dumpsys cacheinfo
  ...
  Cache Name: cache_key.is_compat_change_enabled
    Property: cache_key.is_compat_change_enabled
    Hits: 1301458, Misses: 21387, Skips: 0, Clears: 39
    Skip-corked: 0, Skip-unset: 0, Skip-bypass: 0, Skip-other: 0
    Nonce: 0x856e911694198091, Invalidates: 72, CorkedInvalidates: 0
    Current Size: 1254, Max Size: 2048, HW Mark: 2049, Overflows: 310
    Enabled: true
  ...

Kolom

Hit:

  • Definisi: Frekuensi bagian data yang diminta berhasil ditemukan dalam cache.
  • Signifikansi: Menunjukkan pengambilan data yang efisien dan cepat, sehingga mengurangi pengambilan data yang tidak perlu.
  • Jumlah yang lebih tinggi umumnya lebih baik.

Menghapus:

  • Definisi: Frekuensi cache dihapus karena pembatalan validasi.
  • Alasan Pembersihan:
    • Pembatalan validasi: Data dari server sudah tidak berlaku.
    • Pengelolaan Ruang: Membuat ruang untuk data baru saat cache penuh.
  • Jumlah yang tinggi dapat menunjukkan data yang sering berubah dan potensi inefisiensi.

Kesalahan:

  • Definisi: Frekuensi cache gagal memberikan data yang diminta.
  • Penyebab:
    • Penyimpanan dalam cache yang tidak efisien: Cache terlalu kecil atau tidak menyimpan data yang tepat.
    • Data yang sering berubah.
    • Permintaan pertama kali.
  • Jumlah yang tinggi menunjukkan potensi masalah penyimpanan dalam cache.

Lewati:

  • Definisi: Instance saat cache tidak digunakan sama sekali, meskipun dapat digunakan.
  • Alasan untuk melewati:
    • Corking: Khusus untuk update Android Package Manager, sengaja menonaktifkan penyimpanan dalam cache karena volume panggilan yang tinggi selama booting.
    • Tidak ditetapkan: Cache ada, tetapi tidak diinisialisasi. Nonce tidak ditetapkan, yang berarti cache belum pernah dibatalkan validasinya.
    • Lewati: Keputusan yang disengaja untuk melewati cache.
  • Jumlah yang tinggi menunjukkan potensi inefisiensi dalam penggunaan cache.

Membatalkan validasi:

  • Definisi: Proses menandai data yang di-cache sebagai sudah tidak berlaku atau tidak berlaku lagi.
  • Signifikansi: Memberikan sinyal bahwa sistem berfungsi dengan data terbaru, sehingga mencegah error dan inkonsistensi.
  • Biasanya dipicu oleh server yang memiliki data.

Ukuran Saat Ini:

  • Definisi: Jumlah elemen saat ini dalam cache.
  • Signifikansi: Menunjukkan penggunaan resource cache dan potensi dampaknya terhadap performa sistem.
  • Nilai yang lebih tinggi umumnya berarti lebih banyak memori yang digunakan oleh cache.

Ukuran Maksimum:

  • Definisi: Jumlah maksimum ruang yang dialokasikan untuk cache.
  • Signifikansi: Menentukan kapasitas cache dan kemampuannya untuk menyimpan data.
  • Menetapkan ukuran maksimum yang sesuai akan membantu menyeimbangkan efektivitas cache dengan penggunaan memori. Setelah ukuran maksimum tercapai, elemen baru akan ditambahkan dengan mengeluarkan elemen yang paling jarang digunakan, yang dapat menunjukkan inefisiensi.

Tanda Air Tinggi:

  • Definisi: Ukuran maksimum yang dicapai oleh cache sejak pembuatannya.
  • Signifikansi: Memberikan insight tentang penggunaan cache puncak dan potensi tekanan memori.
  • Memantau nilai maksimum dapat membantu mengidentifikasi potensi bottleneck atau area untuk pengoptimalan.

Overflow:

  • Definisi: Frekuensi cache melebihi ukuran maksimumnya dan harus menghapus data untuk memberi ruang bagi entri baru.
  • Signifikansi: Menunjukkan tekanan cache dan potensi penurunan performa akibat penghapusan data.
  • Jumlah overflow yang tinggi menunjukkan bahwa ukuran cache mungkin perlu disesuaikan atau strategi cache perlu dievaluasi ulang.

Statistik yang sama juga dapat ditemukan dalam laporan bug.

Menyesuaikan ukuran cache

Cache memiliki ukuran maksimum. Jika ukuran cache maksimum terlampaui, entri akan dihapus dalam urutan LRU.

  • Menyimpan terlalu sedikit entri ke dalam cache dapat berdampak negatif pada rasio hit cache.
  • Menyimpan terlalu banyak entri dalam cache akan meningkatkan penggunaan memori cache.

Temukan keseimbangan yang tepat untuk kasus penggunaan Anda.

Menghapus panggilan klien yang tidak perlu

Klien dapat membuat kueri yang sama ke server beberapa kali dalam jangka waktu singkat:

public void executeAll(List<Operation> operations) throws SecurityException {
    for (Operation op : operations) {
        for (Permission permission : op.requiredPermissions()) {
            if (!permissionChecker.checkPermission(permission, ...)) {
                throw new SecurityException("Missing permission " + permission);
            }
        }
        op.execute();
  }
}

Pertimbangkan untuk menggunakan kembali hasil dari panggilan sebelumnya:

public void executeAll(List<Operation> operations) throws SecurityException {
    Set<Permission> permissionsChecked = new HashSet<>();
    for (Operation op : operations) {
        for (Permission permission : op.requiredPermissions()) {
            if (!permissionsChecked.add(permission)) {
                if (!permissionChecker.checkPermission(permission, ...)) {
                    throw new SecurityException(
                            "Missing permission " + permission);
                }
            }
        }
        op.execute();
  }
}

Pertimbangkan memoisasi sisi klien dari respons server terbaru

Aplikasi klien dapat membuat kueri API dengan kecepatan lebih cepat daripada server API yang dapat menghasilkan respons baru yang signifikan. Dalam hal ini, pendekatan yang efektif adalah memaketkan respons server yang terakhir dilihat di sisi klien beserta stempel waktu, dan menampilkan hasil yang di-cache tanpa mengkueri server jika hasil yang di-cache cukup baru. Penulis klien API dapat menentukan durasi memoisasi.

Misalnya, aplikasi dapat menampilkan statistik traffic jaringan kepada pengguna dengan mengkueri statistik di setiap frame yang digambar:

@UiThread
private void setStats() {
    mobileRxBytesTextView.setText(
        Long.toString(TrafficStats.getMobileRxBytes()));
    mobileRxPacketsTextView.setText(
        Long.toString(TrafficStats.getMobileRxPackages()));
    mobileTxBytesTextView.setText(
        Long.toString(TrafficStats.getMobileTxBytes()));
    mobileTxPacketsTextView.setText(
        Long.toString(TrafficStats.getMobileTxPackages()));
}

Aplikasi dapat menggambar frame pada 60 Hz. Namun secara hipotetis, kode klien di TrafficStats dapat memilih untuk mengkueri server untuk mendapatkan statistik maksimal sekali per detik, dan jika dikueri dalam satu detik dari kueri sebelumnya, tampilkan nilai yang terakhir dilihat. Hal ini diizinkan karena dokumentasi API tidak memberikan kontrak terkait keaktualan hasil yang ditampilkan.

participant App code as app
participant Client library as clib
participant Server as server

app->clib: request @ T=100ms
clib->server: request
server->clib: response 1
clib->app: response 1

app->clib: request @ T=200ms
clib->app: response 1

app->clib: request @ T=300ms
clib->app: response 1

app->clib: request @ T=2000ms
clib->server: request
server->clib: response 2
clib->app: response 2

Pertimbangkan codegen sisi klien, bukan kueri server

Jika hasil kueri dapat diketahui oleh server pada waktu build, pertimbangkan apakah hasil tersebut juga dapat diketahui oleh klien pada waktu build, dan pertimbangkan apakah API dapat diterapkan sepenuhnya di sisi klien.

Pertimbangkan kode aplikasi berikut yang memeriksa apakah perangkat adalah smartwatch (yaitu, perangkat menjalankan Wear OS):

public boolean isWatch(Context ctx) {
    PackageManager pm = ctx.getPackageManager();
    return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}

Properti perangkat ini diketahui pada waktu build, khususnya pada saat Framework dibuat untuk image booting perangkat ini. Kode sisi klien untuk hasSystemFeature dapat langsung menampilkan hasil yang diketahui, bukan mengkueri layanan sistem PackageManager jarak jauh.

Menghapus duplikat callback server di klien

Terakhir, klien API dapat mendaftarkan callback ke server API untuk menerima notifikasi peristiwa.

Aplikasi biasanya mendaftarkan beberapa callback untuk informasi dasar yang sama. Daripada meminta server untuk memberi tahu klien satu kali per callback terdaftar menggunakan IPC, library klien harus memiliki satu callback terdaftar menggunakan IPC dengan server, lalu memberi tahu setiap callback terdaftar di aplikasi.

digraph d_front_back {
  rankdir=RL;
  node [style=filled, shape="rectangle", fontcolor="white" fontname="Roboto"]
  server->clib
  clib->c1;
  clib->c2;
  clib->c3;

  subgraph cluster_client {
    graph [style="dashed", label="Client app process"];
    c1 [label="my.app.FirstCallback" color="#4285F4"];
    c2 [label="my.app.SecondCallback" color="#4285F4"];
    c3 [label="my.app.ThirdCallback" color="#4285F4"];
    clib [label="android.app.FooManager" color="#F4B400"];
  }

  subgraph cluster_server {
    graph [style="dashed", label="Server process"];
    server [label="com.android.server.FooManagerService" color="#0F9D58"];
  }
}