Topic: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Bonjour,

Comme le titre l'indique, j'ai un script PHP qui prend un peu de temps car il modifie tous les scores des joueurs d'une équipe. J'ai essayé d'optimiser, mais quoi que je fasse le temps d'enregistrement peut aller vers les 30 secondes... et parfois, j'obtiens un timeout 503...

Après recherches (car tout ça est un peu au-dessus de mes modestes moyens de développeur amateur), j'ai crû comprendre que le problème était l'envoi d'aucune information au navigateur pour maintenir la connexion.

Je me suis odnc mis en tête d'essayer d'envoyer les infos en continu avec ce genre de code :

/**
 * Output span with progress.
 *
 * @param $current integer Current progress out of total
 * @param $total   integer Total steps required to complete
 * @param $message   string Message to display
 */
function outputProgress($current, $total, $message='Please wait') {
  echo '<div class="well text-center" style="position: absolute; width:100%; z-index:$current;">';
  echo '<span>'.$message.'</span>';
  echo '&nbsp;<span>('.round($current / $total * 100).'%)</span>';
  echo '</div>';
  myFlush();
  sleep(1);
}

/**
 * Flush output buffer
 */
function myFlush() {
  echo(str_repeat(' ', 256));
  if (@ob_get_contents()) {
    @ob_end_flush();
  }
  ob_flush();
  flush();
}

et en appelant outputProgress() dans ma boucle gérant l'enregistrement des scores pour chaque joueur. L'idée est donc d'afficher un % à chaque joueur.

Tout fonctionne très bien sur mon localhost, cool! MAIS... sur le serveur, l'enregistrement patauge comme avant et rien ne s'affiche avant la fin du script. Bref, c'est raté :(

J'ai tenté tout ce que je pouvais trouver sur Internet sans succès...

Donc je m'adresse à vous, techniciens avertis et fins connaisseurs de tout ça : auriez-vous une idée sur ce que je peux faire ???

Merci d'avance pour votre aide, et n'hésitez pas à me demander s'il vous faut des informations complémentaires.

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Hello,

Tu veux modifier combien de valeurs, en gros ? Je suppose que ta question porte sur http://planetalert.tuxfamily.org/newsboard/ qui ne semble pas bien rapide pour le peu d'informations qu'il affiche.

Du coup, on peut te donner quelques tuyaux pour ta question de base :
- c'est assez suspect tes ob_get_contents(), ob_flush() sans un ob_start() de ta part ; en fait, je ne suis même pas sûr que tu aies vraiment besoin des fonctions ob_* pour parvenir à tes fins.
- le str_repeat(' ', 256) ne fonctionne probablement pas parce que la sortie de PHP est utilisée pour constituer une réponse HTTP qui est elle-même bufferisée par un serveur Apache puis par un serveur nginx... du coup, ton 256 est un peu faiblard, tu peux y aller à grands coups de 16384 pour outrepasser la plupart des configs par défaut... mais ton code restera susceptible de se retrouver coincé dans un buffer quelconque sur d'autres environnements.
- tu peux aussi t'amuser avec l'en-tête "X-Accel-Buffering" reconnu par nginx ( http://nginx.org/en/docs/http/ngx_http_ … _buffering ) mais là, encore, ça n'est pas une mesure efficace dans tous les cas.

Au-delà se posent les questions de la façon dont les navigateurs reçoivent les infos (par exemple, 16384 espaces, une fois gzippés en cours de route, ça n'a plus forcément le même effet) et du rythme auxquels ils les interprètent.
Globalement, quand un traitement est vraiment long, il est d'usage de l'exécuter de manière asynchrone ; chez TuxFamily, ça peut prendre la forme d'un cron job. Une autre approche consiste à découper le traitement en petits morceaux (des lots de 5 joueurs par exemple) et à exécuter chaque morceau, typiquement un par un, via des requêtes HTTP émises en JavaScript (plus connu sous le nom d'AJAX) ; avec cette approche, c'est du JavaScript côté client qui se charge de demander au serveur de faire un certain travail, d'attendre la réponse pour ce travail et en fonction de cette réponse (HTTP 200 vs HTTP 50x) mettre à jour un petite barre de progression).

Voilà pour répondre à ta question technique au sens strict du terme. Maintenant, notre ressenti, c'est que tu utilises un CMS en guise de couche d'abstraction, probablement pour éviter de toucher à certaines technos que tu ne maîtrises pas ou mal (au hasard : SQL ?), et que cette couche d'abstraction est en train de te poignarder dans le dos en termes de performance (et on ne te cache pas que c'est SUPER-COURANT). Du coup, nous t'invitons non pas à te prendre la tête avec des histoires de buffering, de batchs, de traitements asynchrones, etc. mais plutôt à attaquer le problème à la source.
Ainsi, tu mentionnes une erreur 50x au bout de 30 secondes. En l'état actuel des limitations en place chez TuxFamily, un processus PHP peut s'exécuter pendant beaucoup plus longtemps que ça avant de rencontrer les divers timeouts d'Apache et nginx... mais par contre, il n'est pas autorisé à "manger" plus de 30 secondes de temps CPU (comprendre 100% CPU pendant 30 secondes). Nous pensons que c'est cette limite que tu atteins, et que tu l'atteins pour modifier un petit nombre d'enregistrements, c-à-d un traitement qui devrait être plié en moins d'une seconde et ne devrait pas nécessiter ce chantier anti-buffering que tu as attaqué.
Si tu nous donnes plus de détails sur ce que tu veux effectuer et comment tu l'as implémenté, nous pourrons te conseiller.

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Bonjour,

Ça pour une réponse, c'est une réponse ! MERCI !!! Je viens de lire tout ça rapidement et j'apprends déjà pleins de trucs... C'est génial. Je retrouve espoir quant à mes soucis :)

Je vais relire tout ça plus consciencieusement dès ce soir et je reviendrai poster plus de détails !

Encore merci d'avoir pris le temps de me donner toutes ces explications.

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Re,

Quelques détails sur ce que je fais :
- la page dont je parle n'est pas le 'Newsboard'. Il s'agit d'un tableau avec la liste des joueurs en lignes et la liste des actions possibles en colonnes. Je coche en général 1 action / joueur et j'envoie le tout. Mon script passe chaque joueur et lors de l'enregistrement de chacun, il met à jour ses scores (entre 8 et 12 compteurs différents selon la situation). Lorsque tous les joueurs sont passé, je re-parcoure tout le monde pour voir si il a des 'morts', auquel cas je re-enregistre 1 action pour tout le monde (1 co-équipier est 'mort'). Ça fonctionne correctement (même si ce n'est pas super rapide) sauf ce problème de Timeout (parfois), si il y a un peu trop de joueurs qui meurent au même moment...
- J'enregistre pour 30 joueurs maximum d'un coup.
- Je souhaite revenir au tableau de l'équipe après l'enregistrement pour que les joueurs (élèves) puissent voir tout de suite l'évolution de leur personnage. À la base, c'est pour ça que je voulais afficher un % en 'synchrone', car je me disais qu'il fallait bien que j'attende que tout soit calculé pour ré-afficher mon tableau.

J'ai testé en vain les 3 premiers points abordés dans la 1ère partie de la réponse ('header X-Accel-Buffering', str  _repeat avec 16384, 'ob_start') Rien à faire, je ne parviens pas à voir l'avancée de mon script au fur et à mesure.

Après, j'ai bien lu la suite la réponse et je comprends bien que je ne comprends pas (vous suivez :) ?) suffisamment les choses concernant cet 'anti-buffering'. Je teste donc beaucoup 'en aveugle' et 'à tâtons' et j'accepte sans soucis la remarque 'Nul besoin d'un tel chantier' pour le peu que je souhaite faire. Donc j'abandonne le 'synchrone'.

J'ai déjà utilisé l'AJAX sur le Newsboard pour retarder le chargement des différents classements tout en affichant la page 'principale'. Ce n'était qu'un début d'optimisation, mais je m'y pencherais à nouveau car c'est vrai que tout ça reste encore très lent.
Je viens de comprendre que je pouvais découper mon nombres de joueurs par paquets pour enregistrer petit à petit et indiquer cet avancement. L'idée me paraît pas mal. Je vais donc tenter cette manip et je vous dirais ce qu'il en est.

Merci aussi pour les explications sur la couche d'abstraction. C'est sûr que le CMS que j'utilise (ProcessWire) me simplifie la vie à bien des niveaux mais doit alourdir les choses par rapport à la quantité de données que j'ai. Mais bon, je pense qu'il m'apporte également toute une interface d'administration sans laquelle mon projet de Planet Alert représenterait un travail infaisable (pour moi) tout en exerçant mon métier à côté. Pour le coup, je suis prêt à concéder une certaine lourdeur pour le moment et je pense sincèrement que le problème ici vient davantage de moi que du CMS ! ProcessWire doit être capable de calculer rapidement tout ce que je souhaite et de gérer les reqûtes SQL, mais moi je dois m'y prendre comme un manche...

Bon, je vais me mettre au boulot sur l'AJAX et vous tiens au courant (pas trop vite, ça peut me prendre du temps, hein!)

Merci !

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

celfred wrote:

Pour le coup, je suis prêt à concéder une certaine lourdeur pour le moment et je pense sincèrement que le problème ici vient davantage de moi que du CMS ! ProcessWire doit être capable de calculer rapidement tout ce que je souhaite et de gérer les requêtes SQL, mais moi je dois m'y prendre comme un manche...

Alors, justement, notre conseil, c'est pas vraiment de dropper le synchrone, au contraire. L'approche asynchrone avec batch JavaScript-driven, c'est un truc overkill, qui représente potentiellement beaucoup de boulot (dans la pratique, ça dépend, peut-être que ProcessWire peut te mâcher le travail, je ne sais pas, mais fais-nous juste confiance quand on te dit que c'est overkill) et qui ne réglerait pas le souci de base, à savoir que 30 secondes de temps CPU pour faire quelques SELECT, quelques additions (oui, je caricature) et claquer quelques requêtes UPDATE vers une base MySQL, c'est pas réglo, pas réglo du tout.
Concrètement, pointe-nous vers ton code qui fait le traitement en question et on se penchera dessus. (rappel : on est admins, on peut aller lire la chose ; on est juste trop fainéants pour se taper toute la lecture). L'idée, c'est de se pencher sur ce qui ne va pas de façon à ce qu'il n'y ait plus besoin de la moindre barre de progression.

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Je ne pense pas que ce soit une caricature, c'est vrai que mes opérations ne sont pas si nombreuses que ça pour un ordinateur...

Je comprend votre inquiétude et je suis désolé pour mon côté "pas réglo, pas réglo du tout". Je vous assure que je ne fais pas exprès et que je passe beaucoup de temps à essayer d'améliorer les choses.
À propos, je viens de passer ma soirée (eh oui, il me faut ça) pour tenter de faire un système de requêtes Ajax tous les 5 joueurs et un affichage en conséquence. On verra bien si ça aide un peu mes timeouts, mais je comprends bien que ça ne résout rien côté 'sur-exploitation' du serveur, désolé.

Pour le code, je ne sais pas trop par quel bout vous guider, mais je vais essayer :
- tous les fichiers concernés se trouvent dans le dossier site/templates/ de mon espace.
- la page adminTable.php correspond au tableau possédant toutes les 'cases' à cocher avec ajout de commentaire possible pour chaque. (franchement, si vous vous y retrouvez avec mes attributs 'name' et tout ça, chapeau !
- le traitement du formulaire se trouve maintenant dans 'scripts/main.js', via la fonction ligne 441 (adminTableForm : submit click) (là aussi, j'ai bidouillé pour retrouver les noms des cases et commentaires qui m'intéressent et n'envoyer que ceux là)
- le traitement proprement dit du formulaire se trouve dans submitForms.php avec le début ligne 127 (if adminTableSubmit...).
- ma fonction principale est updateScore() dans my-functions.php, ligne 59
- l'autre fonction utilisée ici : checkDeath(), toujours dans my-functions.php, ligne 363
- les commandes utilisées du genre $pages->find, ou $player->HP... viennent de ProcessWire. Au cas où, leur site est là : https://processwire.com

Voilà pour ce soir. J'espère que vous parviendrez à trouvez quelque chose sans trop vous prendre la tête... Soyez indulgent envers mon code, je fais ce que je peux :)
Merci !

PS : N'hésitez pas à demander si vous souhaitez d'autres infos.

7 (edited by xavier 2017-01-13 23:52:53)

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Ok, j'ai regardé rapidement. Je pense qu'on est bel et bien dans l'optique d'un code qui fait le travail correctement mais qui le fait de manière trop naïve et tue les performances au passage. Première chose qui tue les performances : updateScore() ne traite qu'un joueur à la fois. Vu sa complexité et le type d'abstractions sur lesquelles tu t'appuies, ça n'est pas particulièrement étonnant, je ne vais donc pas insister sur la question.
updateScore() fait environ 300 lignes donc elle est un peu difficile à approcher. Je te conseillerais de refactorer la chose. Traditionnellement, on essaye de limiter la taille des fonctions à "un écran de hauteur" (~25 lignes traditionnellement... oui c'est vite limitant... tu peux compter 40, on survivra largement). Au-delà, on s'y noie à la relecture. Je note également que updateScore() a tendance à se rappeler elle-même pour effectuer diverses tâches subalternes... Ça ne pose pas de problème en soi vis-à-vis des performances (sauf si tu pars en boucle récursive infinie, ce qui serait carrément un bug), mais mon petit doigt me dit que tu vas le regretter lors de l'étape suivante :D

L'étape suivante, justement. Si je ne m'abuse, tu as pu tester ta solution initiale (avec l'anti-buffering) chez toi (ce que je vais appeler "localhost") ; j'en déduis que la soumission du formulaire ne tourne pas très vite chez toi non plus. Je t'invite donc à diagnostiquer sur localhost ce qui prend du temps dans l'exécution de submitForms.php (typiquement avec xdebug + kcachegrind, mais tu as le choix des armes), parce que, là, comme ça, en parcourant le tas de spaghetti, il m'est difficile de pointer un coupable particulier. Je dirais bien, au pif :
- l'accumulation des appels à find() à mesure que updateScore() se rappelle
- tu manipules des objets variés avec des liens entre eux -- je ne serais pas étonné que ProcessWire n'ait d'autre choix que de réécrire beaucoup d'enregistrements en base de données à la moindre modification.
Mais ce ne sont que des idées en l'air.
Tu voudras peut-être faire ça après le refactoring dans la mesure où analyser une fonction récursive avec xdebug+kcachegrind, c'est, hmm, comment dire, courageux ?

Voilà, voilà. Prends ton temps, je ne serai normalement pas dispo demain pour te conseiller.

Edit: j'oubliais : tous ces fichiers PHP/JS/HTML, tu les versionnes dans un coin avec SVN/GIt/whatever ?

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Bonjour,

Les remarques ne me surprennent pas trop :) J'ai déjà regardé ma fonction updateScore() de longues minutes (heures!) en me disant : "Bon, il faut réduire ! Ça devient ingérable..." Je vais tenter d'y repenser et de trouver comment optimiser tout ça. Je ne garantis pas les 40 lignes tout de suite !
Pour la boucle récursive infinie, j'y ait effectivement veillé, car je l'ai rencontrée bien des fois dans mes périples de programmeur amateur ! Je ne voyais pas trop comment faire autrement pour le moment.
Pour dire l'évolution, au début, checkDeath() n'existait pas et s'opérait directement dans updateScore(). En essayant d'optimiser, j'ai réussi (ça me paraissait un succès :)) à la sortir et à ne l'appeler qu'une fois, à la fin du traitement des joueurs.
Pour le 'versioning', j'ai découvert Git à travers ce projet et donc oui, je l'utilise. Pour le moment, tout se trouve là : https://github.com/celfred/PlanetAlertProfile. Là aussi, l'auto-didacte que je suis manipule l'outil avec... très peu de dextérité, mais je trouve l'outil extraordinaire.
Je me demandais justement quels outils on peut utiliser pour tester le temps pris par certaines fonctions. Je note les références données ci-dessus. Bon, maintenant j'ai bien compris qu'il fallait que je m'attaque d'abord au 'refactoring' car l'avertissement d'être "courageux" me semble s'approcher de "un peu dingue" :)

Merci encore pour les conseils.

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Me revoilà après avoir bossé sur tout ça. Si j'ai bien compris, 'refactorer', c'est 'juste' ré-organiser pour pouvoir mieux se relire, sans forcément optimiser.

Résultat : mes fonctions sont pour le moment toujours aussi lentes, mais j'ai ré-organisé et updateScore() ne fait 'plus que' 80 lignes. Sachant que je suis assez 'verbose' car justement, ça m'aide à m'y retrouver, je ne vois plus tropcomment je peux faire plus court. Tout comme ma nouvelle fonction taskExtraAction() qui est à 91 lignes...

Bref, ma prochaine étape sera d'essayer de comprendre où je perds un temps fou dans ma logique d'enregistrement des scores. En attendant, j'espère que mon code devient un peu plus lisible pour quelqu'un qui le découvre :)
Et je reste preneur de conseils alors n'hésitez pas (si vous avez un peu de temps, bien sûr) !
Merci !

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Pour 'finir', j'ai continué à refactorer le code et j'ai trouvé des erreurs dans mes requêtes. Résultat, plus de 5000 requêtes en arrière-plan qui étaient inutiles... Désolé pour tout ça.
Ce n'est pas encore parfait, mais ça va déjà beaucoup mieux. Je suppose que côté serveur, je dois être 'plus réglo' :)

Je vais maintenant voir ce que je peux gérer à l'aide d'un "cache" afin d'éviter encore une charge serveur.

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Ravi de voir que ça avance. 5000 requêtes en arrière-plan ? On parle bien de requêtes SQL ? Je ne voudrais pas te faire flipper, mais à 500 requêtes SQL pour une requête HTTP, on parle déjà de mastodonte...

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Bon, je pense en effet avoir encore un peu de mal à comprendre exactement comment décrire ce que fait ma "couche d'abstraction" ProcessWire concernant les requêtes SQL, et beaucoup de mal à m'approprier les outils de 'debugging' PHP mentionnés dans les posts ci-dessus (mais je progresse :) ) donc ne vous fiez pas trop à mon vocabulaire...
Toujours est-il que je 'chargeais 5000 'pages' (cf ProcessWire) là où je n'en avais besoin que de 300 (mais le nombre de requêtes SQL derrière est bien moindre car en réalité 1 requête charge plusieurs 'pages' (je ne parle pas de 'pages Web' mais de 'pages' (prononcez peigiz, à l'anglaise) comme les nommes ProcessWire). Donc un 'mastodonte', oui, en quelque sorte c'est ce que devait paraître mon site côté serveur alors qu'il est très modeste en réalité :)
Sur le Forum ProcessWire, ils étaient également très curieux de voir ce fameux site aux '5000 loaded pages' ;)

Donc je vais continuer à apprendre comment analyser tout ça et continuer (je pense) à faire tomber mon besoin de ressources pour faire tourner mon site de mieux en mieux.
Merci encore pour votre soutien et vos explications !

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Ok, je vois le genre. Tiens-nous au courant de tes progrès.

Conseils tardifs par rapport au refactoring et à tes fonctions à 80-91 lignes : l'idée du refactoring est en effet de réorganiser le code, ce qui peut prendre diverses formes selon les objectifs voulus. Ici, on parle d'un profiling, plus exactement d'un profiling avec des outils qui tendent à présenter leurs résultats par fonctions : telle fonction a pris tant de temps (avec ou sans ses appels à d'autres fonctions), telle fonction a été appelée tant de fois, telle fonction a appelé telle autre, etc.
Or, avec des outils qui imposent de travailler ainsi, tu te retrouves sans doute avec un rapport qui te dit que ta fonction principale a été appelé plusieurs fois et a pris telle quantité de temps en tout, telle quantité de temps en moyenne. Notre conseil serait donc de découper strictement ta fonction en plein de petites fonctions, courtes, lisibles, avec un nom reflétant leur tâche "métier" spécifique -- en faisant cela, tu obtiendras des rapports xdebug et des graphes kcachegrind exploitables car pointant directement quelles parties du traitement prennent le plus de temps.
Si elles ont du code en commun, tu dois pouvoir le déporter dans des fonctions communes de type "outil", elles-mêmes appelées depuis les fonctions métier. Et ça ne devrait pas trop gêner le profling puisqu'il est possible d'observer le temps "exclusive" (fonction uniquement) ou "inclusive" (fonction + ses appels d'autres fonctions) avec les outils mentionnés.

Re: Flush PHP fonctionne sur localhost, mais pas sur l'hébergement distant

Pour le moment, mes progrès de 'profiling' sont... quasi-nuls :(
J'essaye d'y comprendre quelque chose à mes rapports Xdebug avec Kcachegrind et... j'y vois une quantité d'informations astronomiques et me perd complètement dans les méandres de Processwire. Et oui, je retrouve tous les 'rouages' internes du CMS que j'utilise, mais je ne parviens pas à isoler/retrouver mes propres fonctions pour m'y retrouver...
Je vais donc continuer tranquillement mes recherches, mais je crois que cela va me prendre beaucoup de temps. Si vous avez des ressources qui aident un peu à mieux s'y retrouver avec tout ça, je suis preneur :)

Merci encore pour tous les conseils.