Graves problèmes de performances de FireMonkey lorsqu’il y a beaucoup de contrôles à l’écran

Nous travaillons déjà avec FireMonkey au bureau. Au bout d’un moment, nous avons remarqué que ce n’était pas si rapide en raison de l’accélération du GPU, selon Embarcadero.

Nous avons donc conçu une application de base uniquement pour tester les performances de FireMonkey. Fondamentalement, c’est un formulaire avec un panneau en bas (alBottom) qui fonctionne comme une barre d’état et un panneau tout client (alClient). Le panneau en bas a une barre de progression et une animation.

Nous avons ajouté une méthode au formulaire qui libère tout contrôle présent dans le panneau de tous les clients et le remplit avec des cellules d’un type personnalisé et un style “survoler” et met à jour l’animation, la barre de progression et la légende du formulaire. progrès satisfaisant. L’information la plus importante est l’heure requirejse.

Enfin, nous avons ajouté cette méthode à OnResize du formulaire, exécutez l’application et maximisez le formulaire (1280×1024).

Le résultat avec XE2 était vraiment lent. Cela a pris environ 11 secondes. De plus, le panneau étant rempli jusqu’à ce que l’application soit prête à recevoir les entrées de l’utilisateur, il y a un délai supplémentaire d’environ 10 secondes (comme le gel). Pour un total de 21 secondes.

Avec XE3, la situation s’est détériorée. Pour la même opération, il a fallu au total 25 secondes (14 + 11 congélation).

Et les rumeurs disent que XE4 sera bien pire que XE3.

Ceci est assez effrayant si l’on considère exactement la même application, en utilisant VCL au lieu de FireMonkey et en utilisant SpeedButtons pour avoir le même “effet de souris” ne prend que 1,5 secondes !!! Le problème réside donc clairement dans certains problèmes internes du moteur FireMonkey.

J’ai ouvert un ticket QC (# 113795) et un ticket (payant) pour le support d’embarcadero mais rien ne résoudra le problème.

Je ne comprends pas sérieusement comment ils peuvent ignorer un tel problème. Car notre entreprise est un casse-tête et une rupture de marché. Nous ne pouvons pas offrir de logiciels commerciaux à nos clients avec des performances aussi médiocres. Plus tôt ou plus tard, nous serons obligés de passer à une autre plate-forme (BTW: le même code Delphi Prism avec WPF prend 1,5 seconde comme celui de la VCL).

Si quelqu’un a une idée de la manière de résoudre le problème ou d’essayer d’améliorer ses performances et souhaite aider, je serais vraiment content.

Merci d’avance.

Bruno Fratini

L’application est la suivante:

unit Performance01Main; interface uses System.SysUtils, System.Types, System.UITypes, System.Rtti, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.Objects; const cstCellWidth = 45; cstCellHeight = 21; type TCell = class(TStyledControl) private function GetText: Ssortingng; procedure SetText(const Value: Ssortingng); function GetIsFocusCell: Boolean; protected FSelected: Boolean; FMouseOver: Boolean; FText: TText; FValue: Ssortingng; procedure ApplyStyle; override; procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override; procedure DoMouseEnter; override; procedure DoMouseLeave; override; procedure ApplyTrigger(TriggerName: ssortingng); published property IsSelected: Boolean read FSelected; property IsFocusCell: Boolean read GetIsFocusCell; property IsMouseOver: Boolean read FMouseOver; property Text: Ssortingng read GetText write SetText; end; TFormFireMonkey = class(TForm) StyleBook: TStyleBook; BottomPanel: TPanel; AniIndicator: TAniIndicator; ProgressBar: TProgressBar; CellPanel: TPanel; procedure FormResize(Sender: TObject); procedure FormActivate(Sender: TObject); protected FFocused: TCell; FEntered: Boolean; public procedure CreateCells; end; var FormFireMonkey: TFormFireMonkey; implementation uses System.Diagnostics; {$R *.fmx} { TCell } procedure TCell.ApplyStyle; begin inherited; ApplyTrigger('IsMouseOver'); ApplyTrigger('IsFocusCell'); ApplyTrigger('IsSelected'); FText:= (FindStyleResource('Text') as TText); if (FText  Nil) then FText.Text := FValue; end; procedure TCell.ApplyTrigger(TriggerName: string); begin StartTriggerAnimation(Self, TriggerName); ApplyTriggerEffect(Self, TriggerName); end; procedure TCell.DoMouseEnter; begin inherited; FMouseOver:= True; ApplyTrigger('IsMouseOver'); end; procedure TCell.DoMouseLeave; begin inherited; FMouseOver:= False; ApplyTrigger('IsMouseOver'); end; function TCell.GetIsFocusCell: Boolean; begin Result:= (Self = FormFireMonkey.FFocused); end; function TCell.GetText: String; begin Result:= FValue; end; procedure TCell.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); var OldFocused: TCell; begin inherited; FSelected:= not(FSelected); OldFocused:= FormFireMonkey.FFocused; FormFireMonkey.FFocused:= Self; ApplyTrigger('IsFocusCell'); ApplyTrigger('IsSelected'); if (OldFocused  Nil) then OldFocused.ApplyTrigger('IsFocusCell'); end; procedure TCell.SetText(const Value: Ssortingng); begin FValue := Value; if Assigned(FText) then FText.Text:= Value; end; { TForm1 } procedure TFormFireMonkey.CreateCells; var X, Y: Double; Row, Col: Integer; Cell: TCell; T: TTime; // Workaround suggested by Himself 1 // Force update only after a certain amount of iterations // LP: Single; // Workaround suggested by Himself 2 // Force update only after a certain amount of milliseconds // Used cross-platform TStopwatch as suggested by LU RD // Anyway the same logic was tested with TTime and GetTickCount // SW: TStopWatch; begin T:= Time; Caption:= 'Creating cells...'; {$REGION 'Issue 2 workaround: Update form size and background'} // Bruno Fratini: // Without (all) this code the form background and area is not updated till the // cells calculation is finished BeginUpdate; Invalidate; EndUpdate; // Workaround suggested by Philnext // replacing ProcessMessages with HandleMessage // Application.HandleMessage; Application.ProcessMessages; {$ENDREGION} // Bruno Fratini: // Update starting point step 1 // Improving performance CellPanel.BeginUpdate; // Bruno Fratini: // Freeing the previous cells (if any) while (CellPanel.ControlsCount > 0) do CellPanel.Controls[0].Free; // Bruno Fratini: // Calculating how many rows and columns can contain the CellPanel Col:= Trunc(CellPanel.Width / cstCellWidth); if (Frac(CellPanel.Width / cstCellWidth) > 0) then Col:= Col + 1; Row:= Trunc(CellPanel.Height / cstCellHeight); if (Frac(CellPanel.Height / cstCellHeight) > 0) then Row:= Row + 1; // Bruno Fratini: // Loop variables initialization ProgressBar.Value:= 0; ProgressBar.Max:= Row * Col; AniIndicator.Enabled:= True; X:= 0; Col:= 0; // Workaround suggested by Himself 2 // Force update only after a certain amount of milliseconds // Used cross-platform TStopwatch as suggested by LU RD // Anyway the same logic was tested with TTime and GetTickCount // SW:= TStopwatch.StartNew; // Workaround suggested by Himself 1 // Force update only after a certain amount of iterations // LP:= 0; // Bruno Fratini: // Loop for fulfill the Width while (X < CellPanel.Width) do begin Y:= 0; Row:= 0; // Bruno Fratini: // Loop for fulfill the Height while (Y = 100) then // Workaround suggested by Himself 2 // Force update only after a certain amount of milliseconds // Used cross-platform TStopwatch as suggested by LU RD // Anyway the same logic was tested with TTime and GetTickCount // if (SW.ElapsedMilliseconds >= 30) then // Workaround suggested by Philnext with Bruno Fratini's enhanchment // Skip forcing refresh when the form is not focused for the first time // This avoid the strange side effect of overlong delay on form open // if FEntered then begin Caption:= 'Elapsed time: ' + FormatDateTime('nn:ss:zzz', Time - T) + ' (min:sec:msec) Cells: ' + IntToStr(Trunc(ProgressBar.Value)); {$REGION 'Issue 4 workaround: Forcing progress and animation visual update'} // Bruno Fratini: // Without the ProcessMessages call both the ProgressBar and the // Animation controls are not updated so no feedback to the user is given // that is not acceptable. By the other side this introduces a further // huge delay on filling the grid to a not acceptable extent // (around 20 minutes on our machines between form maximization starts and // it arrives to a ready state) // Workaround suggested by Philnext // replacing ProcessMessages with HandleMessage // Application.HandleMessage; Application.ProcessMessages; {$ENDREGION} // Workaround suggested by Himself 1 // Force update only after a certain amount of iterations // LP:= ProgressBar.Value; // Workaround suggested by Himself 2 // Force update only after a certain amount of milliseconds // Used cross-platform TStopwatch as suggested by LU RD // Anyway the same logic was tested with TTime and GetTickCount // SW.Reset; // SW.Start; end; end; X:= X + cstCellWidth; Col:= Col + 1; end; // Bruno Fratini: // Update starting point step 2 // Improving performance CellPanel.EndUpdate; AniIndicator.Enabled:= False; ProgressBar.Value:= ProgressBar.Max; Caption:= 'Elapsed time: ' + FormatDateTime('nn:ss:zzz', Time - T) + ' (min:sec:msec) Cells: ' + IntToStr(Trunc(ProgressBar.Value)); // Bruno Fratini: // The following lines are required // otherwise the cells won't be properly paint after maximizing BeginUpdate; Invalidate; EndUpdate; // Workaround suggested by Philnext // replacing ProcessMessages with HandleMessage // Application.HandleMessage; Application.ProcessMessages; end; procedure TFormFireMonkey.FormActivate(Sender: TObject); begin // Workaround suggested by Philnext with Bruno Fratini's enhanchment // Skip forcing refresh when the form is not focused for the first time // This avoid the strange side effect of overlong delay on form open FEntered:= True; end; procedure TFormFireMonkey.FormResize(Sender: TObject); begin CreateCells; end; end. 

J’ai essayé votre code, il faut 00: 10: 439 sur mon PC sur XE3 pour remplir l’écran avec des cellules. En désactivant ces lignes:

  //ProgressBar.Value:= ProgressBar.Value + 1; //Caption:= 'Elapsed time: ' + FormatDateTime('nn:ss:zzz', Time - T) + // ' (min:sec:msec) Cells: ' + IntToStr(Trunc(ProgressBar.Value)); ... //Application.ProcessMessages; 

Cela descend à 00: 00: 106 (!).

La mise à jour des contrôles visuels (tels que ProgressBar ou Form.Caption) est très coûteuse. Si vous pensez vraiment en avoir besoin, ne le faites que toutes les centièmes itérations, ou mieux, seulement 250

Si cela n’aide pas les performances, veuillez exécuter votre code avec ces lignes désactivées et mettre à jour la question.

De plus, j’ai ajouté du code pour tester le temps de repeindre:

 T:= Time; // Bruno Fratini: // The following lines are required // otherwise the cells won't be properly paint after maximizing //BeginUpdate; Invalidate; //EndUpdate; Application.ProcessMessages; Caption := Caption + ', Repaint time: '+FormatDateTime('nn:ss:zzz', Time - T); 

Lorsqu’il est exécuté pour la première fois, la création de tous les contrôles prend 00: 00: 072, la repeinte prend 00: 03: 089. Ce n’est donc pas la gestion des objects mais la première fois que la peinture est lente.

La deuxième fois est beaucoup plus rapide.

Comme il y a une discussion dans les commentaires, voici comment vous effectuez les mises à jour des progrès:

 var LastUpdateTime: cardinal; begin LastUpdateTime := GetTickCount - 250; for i := 0 to WorkCount-1 do begin //... //Do a part of work here if GetTickCount-LastUpdateTime > 250 then begin ProgressBar.Position := i; Caption := IntToStr(i) + ' items done.'; LastUpdateTime := GetTickCount; Application.ProcessMessages; //not always needed end; end; end; 

Je n’ai que XE2 et le code n’est pas exactement le même mais, comme le disent certains, le pb semble être sur le même

Application.ProcessMessages;

ligne. Je suggère donc de “rafraîchir” vos composants avec realign ex:

  ProgressBar.Value:= ProgressBar.Value + 1; Caption:= 'Elapsed time: ' + FormatDateTime('nn:ss:zzz', Time - T) + ' (min:sec:msec) Cells: ' + IntToStr(Trunc(ProgressBar.Value)); // in comment : Application.ProcessMessages; // New lines : realign for all the components needed to be refreshes AniIndicator.Realign; ProgressBar.Realign; 

Sur mon PC, un écran 210 Cells est généré en 0.150 s au lieu de 3.7 s avec le code original, à tester dans votre environnement …

Pourquoi testez-vous

“Repeindre”, “InvalidateRect”, “Scene.EndUpdate”

Je peux voir dans votre code que l’opération la plus coûteuse consiste à recréer des commandes. Et pourquoi faites-vous cela dans l’événement OnResize (peut-être mettre un bouton pour recréer les commandes)

cette boucle seule peut manger 30% du temps d’exécution

  while (CellPanel.ControlsCount > 0) do CellPanel.Controls[0].Free; 

il devrait être comme: (éviter la copie de mémoire de liste après chaque libre)

 for i := CellPanel.ControlsCount - 1 downto 0 do CellPanel.Controls[i].Free; 

et ne pas exécuter ProcessMessages en boucle (ou au moins exécuter seulement à chaque 10ème itération ou à peu près)

utiliser AQTime pour profiler votre code (il affichera ce qui dure longtemps)