Une modification qui a accéléré le chargement des règles de filtrage dans Safari jusqu'à 5 fois
Aujourd'hui, je voudrais vous raconter l'histoire de deux expressions régulières : une petite imprécision dans celles-ci a fini par coûter au monde plus de 50 millions d'heures de temps CPU sur les appareils iOS.
Attention : cet article est plein de détails techniques et peut être difficile à suivre si vous ne savez pas coder.
Le fonctionnement des bloqueurs de contenu dans Safari
Tout d'abord, permettez-moi de vous expliquer comment fonctionnent les bloqueurs de publicités dans Safari. En réalité, AdGuard sur iOS s'appuie sur le mécanisme intégré à Safari appelé Safari Content Blockers.
De nos jours, il existe également une approche alternative prise en charge : declarativeNetRequest (DNR), partiellement compatible avec Chrome et Firefox. Il est intéressant de noter que dans le cas de Safari, les règles DNR sont discrètement traduites en règles Safari Content Blocker en arrière-plan.
La toute première version d'AdGuard pour iOS a été lancée en octobre 2015, et nous avons immédiatement rencontré un problème épineux. Historiquement, les bloqueurs de publicités ont toujours utilisé leur propre syntaxe de règles de filtrage. Elle est puissante et spécialement conçue pour le filtrage Web. Apple, cependant, a introduit sa propre version, très différente de celle à laquelle la communauté était habituée.
Avançons un peu : lorsque Chrome a introduit declarativeNetRequest, ils ont également suivi leur propre voie. Au moins en ce qui concerne la correspondance d'URL, leur syntaxe était beaucoup plus proche de celle que nous connaissions.
Voici un exemple de conversion d'une règle AdGuard standard en syntaxe prise en charge par Safari :
Remarquez ce qui arrive au modèle d'URL. Dans les filtres AdGuard, nous utilisons une syntaxe spéciale de type joker spécialement adaptée aux URL. La raison est simple : les expressions régulières traditionnelles sont trop lentes pour cette tâche.
Expressions régulières
Les bloqueurs de contenu Safari, quant à eux, s'appuient sur des expressions régulières, bien que sous une forme très simplifiée, afin de pouvoir être compilés dans une structure qui accélère la correspondance.
Si vous souhaitez en savoir plus sur le fonctionnement interne : Safari crée un bytecode DFA à partir de modèles d'expressions régulières, qui est ensuite exécuté par un interpréteur personnalisé : DFABytecodeInterpreter.cpp.
Les expressions régulières sont plus polyvalentes que la syntaxe standard des bloqueurs de publicités. Malheureusement, cette flexibilité n'est pas vraiment pertinente pour le filtrage de contenu web. Au lieu de cela, nous obtenons une compilation de règles très lente qui consomme des ressources, ainsi que des limites strictes sur le nombre de règles que les filtres peuvent contenir. Nous avons déjà écrit un article sur les problèmes de Safari, et la plupart d'entre eux persistent.
Conversion des modèles v1.0
En 2015, nous avons dû trouver un moyen de convertir les modèles d'URL d'AdGuard en expressions régulières acceptées par Safari.
Nous étions alors confrontés à deux tâches majeures.
Tout d'abord, nous devions réduire le nombre total de règles dans l'ensemble final. À l'époque, Safari avait une limite stricte de 50 000 règles. (Nous avons décrit comment nous avons résolu ce problème dans notre article sur le blocage des publicités dans Safari).
À propos, la limite actuelle a été portée à 150 000. Cependant, en raison des limites de mémoire du processus, vous ne pouvez en utiliser qu'environ 60 à 80 000 dans la pratique. Nous avons signalé ce problème à Apple à plusieurs reprises (rapports Apple Feedback Assistant : FB19728743, FB13282146), mais en vain.
La deuxième tâche consistait à s'assurer que les expressions régulières que nous générions étaient suffisamment efficaces pour fonctionner rapidement et suffisamment légères pour que iOS puisse les compiler. À l'époque, nous avons parfois vu le système tuer le processus « com.apple.Safari.ContentBlockerLoader » parce qu'il consommait trop de ressources.
Après de nombreux tests manuels, nous avons arrêté notre choix sur ce qui semblait être les règles de conversion optimales :
- Le symbole
||
(« début de l'URL ») est devenu^[htpsw]+://([a-z0-9-]\.)?
- Le symbole
^
(« séparateur ») est devenu[/:&?]
Nous étions convaincus d'avoir bien fait notre travail, alors nous avons cessé de nous en préoccuper et avons laissé les choses telles quelles pendant près de dix ans.
Nous avions tort
Tout a commencé avec un autre rapport de bogue. Le problème était que notre méthode de conversion standard modifiait légèrement la sémantique du symbole spécial ||
. Sur iOS, il ne correspondait finalement qu'à un seul niveau de sous-domaine, alors que dans toutes les autres versions d'AdGuard, il correspondait à tous les niveaux.
La solution simple consistait à utiliser l'expression régulière initialement suggérée par les développeurs de WebKit en 2015, celle que nous avions rejetée à l'époque comme « non optimale ». Mais nous étions tellement sûrs de notre choix à l'époque que nous n'avons même pas pris la peine de le revérifier jusqu'à récemment.
Les modifications que nous aurions dû apporter étaient extrêmement simples :
- Remplacer
||
par^[^:]+://+([^:/]+\.)?
- Remplacer
^
dans la plupart des cas par[/:]
Mais y avait-il vraiment une telle différence ? Il s'avère que oui, il y en avait une.
Oh là là, comme nous avions tort !
Après avoir remplacé les nouvelles expressions régulières, nous avons effectué quelques tests rapides et les résultats nous ont époustouflés. La vitesse de chargement des règles dans Safari ne s'est pas seulement améliorée, elle a explosé.
En chiffres : la compilation du filtre de protection contre le pistage dans Safari est devenue 5,5 fois plus rapide, et celle du filtre de base 2,8 fois plus rapide.
Pour vous donner une idée plus concrète, regardez la vidéo ci-dessous :
Si l'on additionne le nombre d'utilisateurs d'AdGuard au cours de la dernière décennie et le nombre de fois où l'application a dû recompiler les filtres, le temps CPU gaspillé s'élève à au moins 50 millions d'heures supplémentaires sur les appareils iOS.
Je suis sincèrement honteux de cette erreur, d'autant plus que la solution correcte était sous nos yeux depuis le début. Avec le recul, il est évident que ces nouvelles expressions régulières allaient être compilées et exécutées plus rapidement que celles que nous avions choisies.
Alors, quelle a été notre erreur à l'époque ? Je pense que tout repose sur une méthodologie de test défaillante. Nous avons essayé de juger « à l'œil nu », au lieu de :
- Définir un ensemble de critères clairs : utilisation de la mémoire, vitesse et performances réelles du bloqueur de contenu dans le navigateur.
- Et surtout : apprendre à mesurer ces éléments avec précision. Non pas en observant Activity Monitor ou
top
, mais en utilisant un profileur approprié.
La bonne nouvelle, c'est que ce problème est désormais résolu. Et j'espère vraiment que nous en avons tiré les leçons nécessaires, afin de ne plus répéter ce genre d'erreurs à l'avenir.