Les appels d'API Android impliquent généralement une latence et un calcul importants par appel. Le cache côté client est donc un élément important à prendre en compte pour concevoir des API utiles, correctes et performantes.
Motivation
Les API exposées aux développeurs d'applications dans le SDK Android sont souvent implémentées en tant que code client dans le framework Android, qui effectue un appel IPC Binder à un service système dans un processus de plate-forme, dont la tâche consiste à effectuer des calculs et à renvoyer un résultat au client. La latence de cette opération est généralement dominée par trois facteurs:
- Coût supplémentaire de l'IPC: un appel IPC de base est généralement 10 000 fois supérieur à la latence d'un appel de méthode de base dans le processus.
- Contention côté serveur: le travail effectué dans le service système en réponse à la requête du client peut ne pas commencer immédiatement, par exemple si un thread de serveur est occupé à traiter d'autres requêtes arrivées plus tôt.
- Calcul côté serveur: le travail lui-même pour gérer la requête sur le serveur peut nécessiter un travail important.
Vous pouvez éliminer ces trois facteurs de latence en implémentant un cache côté client, à condition que le cache soit:
- C'est exact: le cache côté client ne renvoie jamais de résultats différents de ceux que le serveur aurait renvoyés.
- Efficace: les requêtes client sont souvent diffusées à partir du cache, par exemple, le cache a un taux de réussite élevé.
- Efficace: le cache côté client utilise efficacement les ressources côté client, par exemple en représentant les données mises en cache de manière compacte et en ne stockant pas trop de résultats mis en cache ni de données obsolètes dans la mémoire du client.
Envisager de mettre en cache les résultats du serveur dans le client
Si les clients envoient souvent la même requête plusieurs fois et que la valeur renvoyée ne change pas au fil du temps, vous devez implémenter un cache dans la bibliothèque cliente, avec les clés correspondant aux paramètres de la requête.
Envisagez d'utiliser IpcDataCache
dans votre implémentation:
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);
}
}
Pour obtenir un exemple complet, consultez android.app.admin.DevicePolicyManager
.
IpcDataCache
est disponible pour tout le code système, y compris les modules principaux.
Il existe également PropertyInvalidatedCache
, qui est presque identique, mais n'est visible que par le framework. Privilégiez IpcDataCache
dans la mesure du possible.
Invalider les caches en cas de modification côté serveur
Si la valeur renvoyée par le serveur peut changer au fil du temps, implémentez un rappel pour observer les modifications et enregistrez un rappel afin de pouvoir invalider le cache côté client en conséquence.
Invalider les caches entre les scénarios de test unitaire
Dans une suite de tests unitaires, vous pouvez tester le code client avec un double de test plutôt qu'avec le serveur réel. Si c'est le cas, veillez à vider les caches côté client entre les cas de test. Cela permet de maintenir les scénarios de test hermétiques les uns par rapport aux autres et d'éviter qu'un scénario de test n'interfère avec un autre.
@RunWith(AndroidJUnit4.class)
public class BirthdayManagerTest {
@Before
public void setUp() {
BirthdayManager.clearCache();
}
@After
public void tearDown() {
BirthdayManager.clearCache();
}
...
}
Lorsque vous écrivez des tests CTS qui exercent un client d'API qui utilise le cache en interne, le cache est un détail d'implémentation qui n'est pas exposé à l'auteur de l'API. Par conséquent, les tests CTS ne devraient pas nécessiter de connaissances particulières sur le cache utilisé dans le code client.
Étudier les succès et les défauts de cache
IpcDataCache
et PropertyInvalidatedCache
peuvent imprimer des statistiques en direct:
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
...
Champs
Vues :
- Définition: nombre de fois où une partie de données demandée a été trouvée dans le cache.
- Signification: indique une récupération efficace et rapide des données, ce qui réduit la récupération de données inutile.
- Plus le nombre est élevé, mieux c'est.
Effacer:
- Définition: nombre de fois où le cache a été effacé en raison d'une invalidation.
- Motif de la suppression :
- Invalidation: données obsolètes du serveur.
- Gestion de l'espace: libère de l'espace pour les nouvelles données lorsque le cache est plein.
- Un nombre élevé de requêtes peut indiquer que les données changent fréquemment et que l'inefficacité est possible.
Manquements:
- Définition: nombre de fois où le cache n'a pas pu fournir les données demandées.
- Causes :
- Mise en cache inefficace: le cache est trop petit ou ne stocke pas les bonnes données.
- Données changeant fréquemment
- Requêtes effectuées pour la première fois
- Un nombre élevé de requêtes indique des problèmes de mise en cache potentiels.
Sauts:
- Définition: Instances où le cache n'a pas du tout été utilisé, même s'il aurait pu l'être.
- Motifs de la suppression :
- Bouchon: spécifique aux mises à jour du gestionnaire de paquets Android, désactivation délibérée de la mise en cache en raison d'un volume élevé d'appels au démarrage.
- Non défini: le cache existe, mais n'est pas initialisé. Le nonce n'a pas été défini, ce qui signifie que le cache n'a jamais été invalidé.
- Contournement: décision intentionnelle de contourner le cache.
- Un nombre élevé indique des inefficacités potentielles dans l'utilisation du cache.
Invalide les éléments suivants:
- Définition: processus consistant à marquer les données mises en cache comme obsolètes ou obsolètes.
- Signification: indique que le système fonctionne avec les données les plus à jour, ce qui évite les erreurs et les incohérences.
- En général, déclenché par le serveur propriétaire des données.
Taille actuelle:
- Définition: nombre actuel d'éléments dans le cache.
- Signification: Indique l'utilisation des ressources du cache et l'impact potentiel sur les performances du système.
- Des valeurs plus élevées signifient généralement que le cache utilise davantage de mémoire.
Taille maximale:
- Définition: quantité maximale d'espace allouée au cache.
- Signification: Détermine la capacité du cache et sa capacité à stocker des données.
- Définir une taille maximale appropriée permet d'équilibrer l'efficacité du cache avec l'utilisation de la mémoire. Une fois la taille maximale atteinte, un nouvel élément est ajouté en supprimant l'élément le moins utilisé récemment, ce qui peut indiquer une inefficacité.
Point haut:
- Définition: Taille maximale atteinte par le cache depuis sa création.
- Signification: fournit des insights sur l'utilisation maximale du cache et la pression de mémoire potentielle.
- Surveiller le niveau maximal peut vous aider à identifier les goulots d'étranglement potentiels ou les domaines à optimiser.
Dépassements :
- Définition: nombre de fois où le cache a dépassé sa taille maximale et a dû supprimer des données pour laisser de la place à de nouvelles entrées.
- Signification: Indique la pression sur le cache et la dégradation potentielle des performances due à l'éviction des données.
- Un nombre élevé de débordements suggère que la taille du cache doit peut-être être ajustée ou que la stratégie de mise en cache doit être réévaluée.
Les mêmes statistiques sont également disponibles dans un rapport de bug.
Ajuster la taille du cache
Les caches ont une taille maximale. Lorsque la taille maximale du cache est dépassée, les entrées sont supprimées dans l'ordre LRU.
- Le fait de mettre en cache trop peu d'entrées peut avoir un impact négatif sur le taux de succès de cache.
- Mettre en cache trop d'entrées augmente l'utilisation de la mémoire du cache.
Trouvez le juste équilibre pour votre cas d'utilisation.
Éliminer les appels client redondants
Les clients peuvent envoyer plusieurs fois la même requête au serveur sur une courte période:
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();
}
}
Envisagez de réutiliser les résultats des appels précédents:
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();
}
}
Envisager la mémorisation côté client des réponses récentes du serveur
Les applications clientes peuvent interroger l'API à un rythme plus rapide que le serveur de l'API ne peut produire de nouvelles réponses significatives. Dans ce cas, une approche efficace consiste à mettre en cache la dernière réponse du serveur côté client avec un code temporel, puis à renvoyer le résultat mis en cache sans interroger le serveur si le résultat mis en cache est suffisamment récent. L'auteur du client de l'API peut déterminer la durée de la mémorisation.
Par exemple, une application peut afficher des statistiques sur le trafic réseau à l'utilisateur en interrogeant les statistiques dans chaque frame dessiné:
@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()));
}
L'application peut dessiner des images à 60 Hz. Toutefois, de manière hypothétique, le code client dans TrafficStats
peut choisir d'interroger le serveur pour obtenir des statistiques au maximum une fois par seconde et, en cas de requête dans la seconde d'une requête précédente, renvoyer la dernière valeur vue.
Cela est autorisé, car la documentation de l'API ne fournit aucun contrat concernant la fraîcheur des résultats renvoyés.
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
Envisager la génération de code côté client au lieu des requêtes côté serveur
Si les résultats de la requête sont connus du serveur au moment de la compilation, demandez-vous s'ils sont également connus du client au moment de la compilation, et si l'API peut être entièrement implémentée côté client.
Prenons l'exemple de code d'application suivant, qui vérifie si l'appareil est une montre (c'est-à-dire qu'il exécute Wear OS):
public boolean isWatch(Context ctx) {
PackageManager pm = ctx.getPackageManager();
return pm.hasSystemFeature(PackageManager.FEATURE_WATCH);
}
Cette propriété de l'appareil est connue au moment de la compilation, en particulier au moment où le Framework a été compilé pour l'image de démarrage de cet appareil. Le code côté client pour hasSystemFeature
peut renvoyer immédiatement un résultat connu, plutôt que d'interroger le service système PackageManager
distant.
Dédupliquer les rappels de serveur dans le client
Enfin, le client de l'API peut enregistrer des rappels auprès du serveur de l'API pour être informé des événements.
Il est courant que les applications enregistrent plusieurs rappels pour les mêmes informations sous-jacentes. Plutôt que de demander au serveur d'informer le client une fois par rappel enregistré à l'aide de l'IPC, la bibliothèque cliente doit disposer d'un rappel enregistré à l'aide de l'IPC avec le serveur, puis informer chaque rappel enregistré dans l'application.
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"];
}
}