Les appels d'API Android impliquent généralement une latence et un calcul importants par invocation. La mise en cache côté client est donc un élément important à prendre en compte lors de la conception d'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. Ce service est chargé d'effectuer un calcul et de renvoyer un résultat au client. La latence de cette opération est généralement dominée par trois facteurs :
- Surcharge IPC : un appel IPC de base est généralement 10 000 fois plus lent qu'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 démarrer 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 traiter 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 :
- Correct : 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 présente un taux de succès é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 sur le client
Si les clients effectuent souvent exactement 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, indexé par les 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 lorsque cela est possible.
Invalider les caches en cas de modifications 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-le afin de pouvoir invalider le cache côté client en conséquence.
Invalider les caches entre les cas de test unitaire
Dans une suite de tests unitaires, vous pouvez tester le code client par rapport à un double de test plutôt qu'au serveur réel. Si tel est le cas, veillez à effacer tous les caches côté client entre les cas de test. Cela permet de maintenir l'hermétisme mutuel des cas de test et d'empêcher un cas de test d'interférer 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 API utilisant la mise en 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 doivent pas nécessiter de connaissances particulières sur la mise en cache utilisée 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
Hits :
- Définition : nombre de fois qu'un élément de données demandé a été trouvé dans le cache.
- Importance : indique une récupération efficace et rapide des données, ce qui réduit la récupération de données inutiles.
- Des nombres plus élevés sont généralement préférables.
Clears :
- Définition : nombre de fois où le cache a été effacé en raison d'une invalidation.
- Raisons de l'effacement :
- Invalidation : données obsolètes du serveur.
- Gestion de l'espace : libérer de l'espace pour de nouvelles données lorsque le cache est plein.
- Un nombre élevé peut indiquer des données qui changent fréquemment et une inefficacité potentielle.
Misses :
- Définition : nombre de fois où le cache n'a pas pu fournir les données demandées.
- Causes:
- Mise en cache inefficace : cache trop petit ou ne stockant pas les bonnes données.
- Données qui changent fréquemment.
- Requêtes effectuées pour la première fois.
- Un nombre élevé suggère des problèmes de mise en cache potentiels.
Skips :
- Définition : instances où le cache n'a pas été utilisé du tout, même s'il aurait pu l'être.
- Raisons de l'omission :
- Corking : spécifique aux mises à jour d'Android Package Manager, 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 d'ignorer le cache.
- Un nombre élevé indique des inefficacités potentielles dans l'utilisation du cache.
Invalidates :
- Définition : processus consistant à marquer les données mises en cache comme obsolètes.
- Importance : fournit un signal indiquant que le système fonctionne avec les données les plus récentes, ce qui évite les erreurs et les incohérences.
- Généralement déclenché par le serveur propriétaire des données.
Current Size :
- Définition : nombre actuel d'éléments dans le cache.
- Importance : 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 plus de mémoire.
Max Size :
- Définition : espace maximal alloué au cache.
- Importance : 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 et 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 récemment utilisé, ce qui peut indiquer une inefficacité.
High Water Mark :
- Définition : taille maximale atteinte par le cache depuis sa création.
- Importance : fournit des informations sur l'utilisation maximale du cache et la pression potentielle sur la mémoire.
- La surveillance du niveau maximal peut aider à identifier les goulots d'étranglement potentiels ou les zones d'optimisation.
Overflows :
- Définition : nombre de fois où le cache a dépassé sa taille maximale et a dû supprimer des données pour faire de la place pour de nouvelles entrées.
- Importance : indique la pression du cache et la dégradation potentielle des performances due à la suppression des données.
- Un nombre élevé de dépassements suggère qu'il peut être nécessaire d'ajuster la taille du cache ou de réévaluer la stratégie de mise en cache.
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.
- La mise en cache d'un nombre trop faible d'entrées peut avoir un impact négatif sur le taux de succès du cache.
- La mise en cache d'un nombre trop élevé d'entrées augmente l'utilisation de la mémoire du cache.
Trouvez le bon équilibre pour votre cas d'utilisation.
Éliminer les appels client redondants
Les clients peuvent effectuer la même requête auprès du serveur plusieurs fois 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émoïsation côté client des réponses récentes du serveur
Les applications clientes peuvent interroger l'API à un rythme plus rapide que celui auquel le serveur de l'API peut produire des réponses nouvelles et significatives. Dans ce cas, une approche efficace consiste à mémoïser la dernière réponse du serveur vue côté client avec un code temporel, et à renvoyer le résultat mémoïsé sans interroger le serveur si le résultat mémoïsé est suffisamment récent. L'auteur du client API peut déterminer la durée de la mémoïsation.
Par exemple, une application peut afficher des statistiques sur le trafic réseau à l'utilisateur en interrogeant les statistiques dans chaque frame dessinée :
@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 frames à 60 Hz. Mais, en théorie, le code client dans TrafficStats peut choisir d'interroger le serveur pour obtenir des statistiques au maximum une fois par seconde et, s'il est interrogé dans la seconde suivant 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 serveur
Si le serveur connaît les résultats de la requête au moment de la compilation, déterminez s'ils sont également connus du client au moment de la compilation et si l'API peut être implémentée entièrement côté client.
Prenons l'exemple du code d'application suivant qui vérifie si l'appareil est une montre (c'est-à-dire si l'appareil 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é créé pour l'image de démarrage de cet appareil. Le code côté client de hasSystemFeature peut renvoyer immédiatement un résultat connu, au lieu d'interroger le service système PackageManager à distance.
Dédoublonner les rappels serveur sur le client
Enfin, le client API peut enregistrer des rappels auprès du serveur 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 d'IPC, la bibliothèque cliente doit avoir un rappel enregistré à l'aide d'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"];
}
}