Résoudre les Problèmes de Débogueur Lent
Autres langues : English Español Deutsch 日本語 한국어 Português 中文
Bien que leur surcharge soit généralement minimale, les débogueurs Java peuvent néanmoins engendrer des coûts d’exécution significatifs dans certaines circonstances. Dans un scénario malheureux, le débogueur peut même complètement bloquer la JVM.
Examinons les raisons de ces problèmes et leurs solutions possibles.
Les détails présentés dans cet article se réfèrent aux IDE JetBrains, et certaines des fonctionnalités mentionnées pourraient ne pas être disponibles dans d’autres outils. Néanmoins, la stratégie générale de dépannage devrait toujours s’appliquer.
Diagnostiquer la cause
Avant d’explorer les solutions possibles, il serait sage d’identifier le problème. Les raisons les plus courantes pour lesquelles le débogueur ralentit une application incluent :
- Utilisation de points d’arrêt de méthode
- Évaluation d’expressions trop fréquemment
- Évaluation d’expressions trop lourdes en termes de calcul
- Débogage à distance avec une latence élevée
IntelliJ IDEA élimine toute conjecture dans l’identification de la cause des problèmes de performance du débogueur en fournissant des statistiques détaillées dans l’onglet Overhead du débogueur :
Pour y accéder, sélectionnez Overhead depuis l’onglet Layout Settings. L’onglet Overhead affichera la liste des points d’arrêt et des fonctionnalités du débogueur. À côté de chaque point d’arrêt ou fonctionnalité, vous pouvez voir combien de fois chaque fonctionnalité du débogueur a été utilisée et le temps qu’il a fallu pour l’exécuter.
Dans l’onglet Overhead, vous pouvez également désactiver temporairement une fonctionnalité consommatrice de ressources en décochant la case correspondante de la fonctionnalité.
Maintenant que nous avons vu comment identifier la source du problème de performance, examinons les causes les plus courantes et comment les résoudre.
Points d’arrêt de méthode
Lors de l’utilisation de points d’arrêt de méthode en Java, vous pourriez connaître des baisses de performance, selon le débogueur que vous utilisez. C’est parce que la fonctionnalité correspondante fournie par la Java Debug Interface est notoirement lente.
Pour résoudre ce problème, IntelliJ IDEA propose des points d’arrêt de méthode émulés. Ceux-ci fonctionnent exactement comme les points d’arrêt de méthode ordinaires, mais de manière plus efficace. Les points d’arrêt de méthode émulés impliquent une astuce sous le capot : au lieu de définir de véritables points d’arrêt de méthode, l’IDE les remplace par des points d’arrêt de ligne réguliers dans toutes les implémentations de la méthode à travers le projet.
Par défaut, tous les points d’arrêt de méthode dans IntelliJ IDEA sont émulés :
Si vous utilisez un débogueur qui n’a pas cette fonctionnalité et que vous rencontrez des problèmes de performance avec les points d’arrêt de méthode, vous pouvez faire la même astuce manuellement. Visiter toutes les implémentations de méthode peut être fastidieux, mais cela peut être rentable en vous faisant gagner du temps lors du débogage.
‘Processing classes for emulated method breakpoints’ prend trop de temps
Si une méthode a un énorme nombre d’implémentations, définir un point d’arrêt de méthode dessus pourrait prendre un certain temps. Dans ce cas, IntelliJ IDEA et Android Studio afficheront une boîte de dialogue indiquant Processing classes for emulated method breakpoints.
Si le processus d’émulation des points d’arrêt de méthode prend trop de temps pour vous, envisagez d’utiliser un point d’arrêt de ligne à la place. Alternativement, vous pouvez sacrifier un peu de performance à l’exécution en décochant la case Emulated dans les paramètres du point d’arrêt.
Points d’arrêt conditionnels dans le code critique
Définir un point d’arrêt conditionnel dans du code critique pourrait considérablement ralentir une session de débogage, selon la fréquence d’exécution de ce code.
Considérez l’extrait de code suivant :
public class Loop {
public static final int ITERATIONS = 100_000;
public static void main(String[] args) {
var start = System.currentTimeMillis();
var sum = 0;
for (int i = 0; i < ITERATIONS; i++) {
sum += i;
}
var end = System.currentTimeMillis();
System.out.println(sum);
System.out.printf("The loop took: %d ms\n", end - start);
}
} const val ITERATIONS = 100_000
fun main() = measureTimeMillis {
var sum = 0
for (i in 0 until ITERATIONS) {
sum += i
}
println(sum)
}.let { println("The loop took: $it ms") } Définissons un point d’arrêt à sum += i et
spécifions false comme condition.
Cela signifie effectivement que le débogueur ne devrait jamais s’arrêter à ce point d’arrêt.
Cependant, chaque fois que cette ligne s’exécute, le débogueur devra évaluer false .
Dans ce cas, les résultats de l’exécution de ce code avec et sans le point d’arrêt étaient respectivement de 39 ms et 29855 ms.
Remarquablement, même avec seulement 100 000 itérations, la différence est énorme !
Il peut être surprenant qu’évaluer une condition apparemment triviale comme false
prenne autant de temps.
C’est parce que le temps écoulé n’est pas seulement dû au calcul du résultat de l’expression.
Il implique également la gestion des événements du débogueur et la communication avec l’interface du débogueur.
La solution est simple. Vous pouvez intégrer la condition directement dans le code de l’application :
public class Loop {
public static final int ITERATIONS = 100_000;
public static void main(String[] args) {
var start = System.currentTimeMillis();
var sum = 0;
for (int i = 0; i < ITERATIONS; i++) {
if (false) { // condition goes here
System.out.println("break") // breakpoint goes here
}
sum += i;
}
var end = System.currentTimeMillis();
System.out.println(sum);
System.out.printf("The loop took: %d ms\n", end - start);
}
} fun main() = measureTimeMillis {
var sum = 0
for (i in 0 until ITERATIONS) {
if (false) { // condition goes here
println("break") // breakpoint goes here
}
sum += i
}
println(sum)
}.let { println("The loop took: $it ms") } Avec cette configuration, la JVM exécutera directement le code de la condition, et elle pourrait même optimiser ce code. Inversement, le débogueur n’entrera en jeu que lorsque le point d’arrêt sera atteint. Bien que ce ne soit pas nécessaire dans la plupart des cas, ce changement peut vous faire gagner du temps si vous devez suspendre conditionnellement le programme au milieu d’un chemin critique.
La technique décrite fonctionne parfaitement avec les classes dont le code source est disponible. Cependant, avec du code compilé, comme les bibliothèques, l’astuce peut être plus difficile à réaliser. C’est un cas d’utilisation spécial, que je couvrirai dans une discussion séparée.
Évaluation implicite
En plus des fonctionnalités – telles que les conditions de points d’arrêt et les watches – où vous spécifiez vous-même les expressions, il existe également des fonctionnalités qui évaluent implicitement les expressions pour vous.
Voici un exemple :
Chaque fois que vous suspendez un programme, le débogueur affiche les valeurs des variables disponibles dans le contexte actuel. Certains types peuvent avoir des structures complexes difficiles à visualiser et à naviguer. Pour votre commodité, le débogueur les transforme à l’aide d’expressions spéciales appelées renderers.
Les renderers peuvent être triviaux, comme toString() , ou plus complexes,
comme ceux qui transforment le contenu des collections. Ils peuvent être intégrés ou personnalisés.
Le débogueur d’IntelliJ IDEA est très flexible dans la façon dont il affiche vos données. L’IDE vous permet même de configurer les renderers à l’aide d’annotations, garantissant des représentations de classe cohérentes à travers un projet avec plusieurs contributeurs.
Pour en savoir plus sur la configuration du format d’affichage des données, consultez la documentation d’IntelliJ IDEA.
Typiquement, la surcharge apportée par les renderers de débogage est négligeable,
mais l’impact dépend finalement du cas d’utilisation particulier.
En effet, si certaines de vos implémentations de toString() contiennent du code pour miner de la crypto,
le débogueur aura du mal à afficher la valeur toString() pour cette classe !
Si le rendu d’une certaine classe s’avère lent, vous pouvez désactiver le renderer correspondant. Comme alternative plus flexible, vous pouvez configurer le renderer pour une utilisation uniquement lorsque nécessaire. Les renderers à la demande ne s’exécuteront que lorsque vous demanderez explicitement d’afficher leur résultat.
Latence élevée dans les sessions de débogage à distance
D’un point de vue technique, déboguer une application à distance n’est pas différent de déboguer localement. De toute façon, la connexion est établie via un socket – nous excluons le mode mémoire partagée de cette discussion – et le débogueur n’est même pas conscient de l’endroit où la JVM hôte s’exécute.
Cependant, un facteur qui pourrait être unique au débogage à distance est la latence du réseau. Certaines fonctionnalités du débogueur effectuent plusieurs allers-retours réseau chaque fois qu’elles sont utilisées. Combinée à une latence élevée, cela peut entraîner une baisse considérable des performances de débogage.
Si c’est le cas, pensez à exécuter le projet localement, car cela pourrait vous faire gagner beaucoup de temps. Sinon, vous pourriez bénéficier de la désactivation temporaire de certaines des fonctionnalités avancées.
Conclusion
Dans cet article, nous avons appris comment résoudre les problèmes les plus courants entraînant de mauvaises performances du débogueur. Bien que l’IDE s’en occupe parfois pour vous, il est important de comprendre les mécanismes sous-jacents. Cela vous permet d’être plus flexible, efficace et créatif dans votre débogage quotidien.
J’espère que vous avez trouvé ces trucs et astuces utiles. Comme toujours, vos commentaires sont grandement appréciés ! N’hésitez pas à me contacter sur X, LinkedIn, ou Telegram.
Bon débogage !