none
Choisir une taille de buffer adaptée, buffer dynamique ? RRS feed

  • Question

  • Bonjour,

    Je développe un outil dont la fonction est de télécharger des emails dans une base SQLite locale.

    Le téléchargement se fait sur un thread séparé via background worker et puisque SQLite ne supporte pas le multi-threading je déclenche un évènement ProgressChanged (traité par le thread principal donc) à chaque message téléchargé.

    Lorsque la connexion est rapide (ce qui est généralement le cas de nos jours), l'interface ne réagit plus car la pile des messages windows devient très vite surchargée, et l'utilisation de DoEvents après chaque message sauvegardé déclenche rapidement un StackOverFlow.

    J'ai donc mis en place un buffer avec une liste d'une certaine capacité qui est remplie au fur et à mesure, puis l'ensemble des messages de la liste est sauvegardé d'un coup et la liste est vidée. C'est très efficace et que le téléchargement se fait sans souci.

    Le problème est que je ne sais pas dans quelles conditions sera executé mon programme :

       - Si l'utilisateur à une connexion lente et un disque rapide un gros buffer est inutile et peu agréable (les messages s'affichent par paquet)

       - S'il a une connexion rapide et un disque lent, le buffer qui me convient sera surement insuffisant pour lui

    D'où ma question : est-il possible de gérer dynamiquement la taille du buffer sans trop perdre en performance ?

    Sinon, quelle méthode appliquer pour essayer de déterminer la taille la plus adaptée ?

    Peut-être même avez-vous d'autres solutions qu'un buffer, elles sont les bienvenues.

    Merci beaucoup,

    Pierre


    • Modifié piher mardi 27 mars 2012 08:19 Post inital pas clair
    lundi 26 mars 2012 14:02

Réponses

  • Bonjour,

    J'arrive un peu comme un cheveu sur la soupe ;-) je ne connais pas SQL Lite mais http://sqlite.org/threadsafe.html laisse pourtant supposer quelques possibilités.

    Egalement si on utilise deux connections séparées (une depuis l'UI, une créée et libérée depuis les traitements de fond) je dirais que les mécanismes habituels d'une base de données devraient s'appliquer et que du point la vue de la base ce ne sont que deux accès concurrents séparés (mais je suis d'accord que ce serait dommage d'insérer les données d'un côté pour les relire de l'autre).

    Pour le backgroundworker, une autre approche pourrait être d'envoyer un "reportprogress" si plus de x secondes se sont écoulées depuis le dernier et/ou si m messages ont été récupérés. De cette façon, l'interface est mise à jour aussi rapidement que possible (à chaque message si c'est lent ou par paquets de plusieurs messages si on a une connexion plus rapide).

    Je ne sais pas d'où sont récupérer les messages (ce sont des mails POP3 ?) mais une approche également assez fréquente est de récupérer les en-têtes et de récupérer le contenu en tâche de fond quitte à récupérer un contenu de façon anticipé à la demande si l'utilisateur clique sur ce message (en gros c'est un client de messagerie ?).

    Rien de bien précis mais peut-être qq pistes qui pourraient aider...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    mercredi 28 mars 2012 11:48
    Modérateur

Toutes les réponses

  • Bonjour,

    Quand l'erreur du StarkOver ce produit il passe quand meme dans le RunWorkerCompleted ?
    si il passe dedans quand l'erreur ce produit c'est possible de modifier le buffer sinon je vois pas comment faire ...

    peut etre que la documentation vous aidera ? http://msdn.microsoft.com/fr-fr/library/system.componentmodel.backgroundworker%28v=vs.95%29.aspx


    Cordialement,
    Xavier TALOUR
    Alias Troxsa SendMail
    Voir le profil de Xavier TALOUR sur LinkedIn

    lundi 26 mars 2012 14:49
    Auteur de réponse
  • Bonjour et merci,

    Excusez-moi j'ai écris trop vite, en fait c'est juste qu'il est primordial que l'utilisateur puisse annuler le téléchargement.

    Or cela devient impossible quand la connexion est rapide car l'interface ne répond plus du tout, l'exception stackOverFlow était levée quand j'avais essayé de contourner le problème par un DoEvents (oui c'est tricher) a chaque message.

    Depuis que j'ai mis en place le buffer l'interface est très réactive, mais j'ai peur qu'avec une connexion plus rapide et un disque plus lent le problème soit au final le même. Et je n'ai clairement pas envie de mettre un gros buffer ce qui consommerait beaucoup de mémoire et afficherai les messages par gros paquets.

    lundi 26 mars 2012 15:10
  • Pourriez vous me donner les lignes de code de comment est fait votre buffer ?

    Une idée en passant il existe l'objet "Queue" qui est un first in First out (premier entrer, premier sortie) qui pourrais etre super utile dans votre cas.

    Vous avez aussi Stack qui pourrais convenir ...


    Cordialement,
    Xavier TALOUR
    Alias Troxsa SendMail
    Voir le profil de Xavier TALOUR sur LinkedIn

    lundi 26 mars 2012 15:19
    Auteur de réponse
  • J'ai fait au plus simple.

    Pour simplifier disons qu'il y a un objet Msg, une méthode getMessage(i) qui se charge de télécharger le message i du serveur et une methode saveMails(List(Of Msg) qui les sauvegarde sur le disque.

    Le buffer est une simple liste dont la capacité est initialisée avant de lancer le worker.

    ça donne :

    Private Sub runworker(sender As Object, e As System.ComponentModel.DoWorkEventArgs)
        For i As Integer = 0 To msgCount - 1
            leWorker.ReportProgress(Nothing, getMessage(i))
        Next
    End Sub
    
    Private bufferSize as Integer = 30
    Private msgBuffer As List(Of Msg)
    Private Sub workerProgress(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs)
        If msgBuffer.Count >= bufferSize Then
            saveMails(msgBuffer)
            msgBuffer.Clear()
        End If
        msgBuffer.Add(e.UserState)
    End Sub

    Puis le RunWorkerCompleted se charge de traiter le buffer restant à la fin.

    lundi 26 mars 2012 15:32
  • Bonjour,

    Pouvez-vous nous montrer le code des méthodes getMessage() et saveMails(). Un StackOverflowException indique le plus souvent une méthode récursive qui s'appelle elle-même de manière infinie.

    Cordialement


    Gilles TOURREAU - MVP C#
    Architecte logiciel/Consultant/Formateur Freelance
    Blog : http://gilles.tourreau.fr
    - MCPD : Enterprise Developper / Windows Developper 3.5 / ASP .NET 3.5/4.0
    - MCITP : SQL Server 2008 Developper
    - MCTS : ADO .NET 3.5 / SQL Server 2008 Developper / Windows Forms 3.5 / ASP .NET 3.5/4.0

    mardi 27 mars 2012 05:44
    Modérateur
  • Bonjour et merci,

    Excusez-moi j'ai écris trop vite, en fait c'est juste qu'il est primordial que l'utilisateur puisse annuler le téléchargement.

    Or cela devient impossible quand la connexion est rapide car l'interface ne répond plus du tout, l'exception stackOverFlow était levée quand j'avais essayé de contourner le problème par un DoEvents (oui c'est tricher) a chaque message.

    Depuis que j'ai mis en place le buffer l'interface est très réactive, mais j'ai peur qu'avec une connexion plus rapide et un disque plus lent le problème soit au final le même. Et je n'ai clairement pas envie de mettre un gros buffer ce qui consommerait beaucoup de mémoire et afficherai les messages par gros paquets.

    Bonjour,

    Merci pour vos suggestions mais je me suis mal exprimé dans mon premier post, cf post cité.

    Mon problème était initialement que lorsque le téléchargement se fait très vite, la sauvegarde des messages un par un consommait toutes les ressources du thread principal et figeait l'UI, rendant le clic sur un bouton "annuler" impossible.

    J'ai essayé de tricher en appelant doevents après chaque message sauvegardé mais le résultat était vite un stackOverFlow.

    J'ai donc mis en place le buffer décrit un peu plus haut et l'interface redevient belle.

    Mon problème est donc de régler la taille du buffer.

    Cordialement

    mardi 27 mars 2012 08:16
  • Bonjour,

    Pour le problème de taille du buffer voir le code que j'ai donné plus haut
    Je preco plus Queue que Stack, un exemple est mis sur le site Microsoft si vous ne comprenez n’hésiter pas a nous le dire et on le fera ensemble

    A bientôt


    Cordialement,
    Xavier TALOUR
    Alias Troxsa SendMail
    Voir le profil de Xavier TALOUR sur LinkedIn

    mardi 27 mars 2012 08:22
    Auteur de réponse
  • Cette piste à l'air intéressante. Je ne suis pas sûr de bien comprendre comment les utiliser dans ce cas précis par contre :

    J'imagine que le plus performant serait du coup d'utiliser un queue synchronisé qui peut donc être rempli directement depuis le backgroundworker sans passer par un évènement ProgressChanged, mais je ne vois pas quand serait vidée la queue ? S'agit-t-il d'utiliser un fonctionnement de type timer sur le thread principal ?

    mardi 27 mars 2012 08:41
  • Je n'ai pas Visual Basic sur cette machine, je vais faire l'installation et vous donner un exemple.

    Queue ce vide tout seul avec une des propriétés (consommer) maintenant ce qu'il faut savoir est si on peux limité la taille max de cette collection

    je reviendrai vers vous avec un exemple.


    Cordialement,
    Xavier TALOUR
    Alias Troxsa SendMail
    Voir le profil de Xavier TALOUR sur LinkedIn

    mardi 27 mars 2012 09:12
    Auteur de réponse
  • Bonjour,

    Je reviens sur votre problème initiale, en fait, vous utilisez la méthode "workerProgress" pour exécutez un traitement, qui est la sauvegarde des emails.

    Ce concept n'est pas correct. Vous devez dans la méthode runworker exécuter votre code complet (get + save des emails + ReportProgress) et dans la méthode workerProgress juste reporter la progression au niveau de l'IHM (barre de progression).

    Le "workerProgress" s'exécute dans le Thread d'affichage, le fait que vous exécutez une méthode SaveMails() dans cette méthode va donc ralentir l'affichage.

    En résumé : Tout le travail se fait dans la méthode runworker, tout l'affichage de l'état du travail dans la méthode "workerProgress".

    En espérant que cela vous aide et simplifie "l'usine à gaz" que vous êtes en train de mettre en place...

    Cordialement


    Gilles TOURREAU - MVP C#
    Architecte logiciel/Consultant/Formateur Freelance
    Blog : http://gilles.tourreau.fr
    - MCPD : Enterprise Developper / Windows Developper 3.5 / ASP .NET 3.5/4.0
    - MCITP : SQL Server 2008 Developper
    - MCTS : ADO .NET 3.5 / SQL Server 2008 Developper / Windows Forms 3.5 / ASP .NET 3.5/4.0

    mardi 27 mars 2012 10:58
    Modérateur
  • Bonjour,

    J'aimerai bien mais c'est impossible :

    Les mails se téléchargent et s'affichent dans un contrôle ListView.

    Ce contrôle reste disponible pendant le téléchargement, l'utilisateur peut donc lire ses mails pendant le téléchargement. Les ListViewItem ne contiennent évidemment pas le message complet, seulement le strict minimum pour l'affichage plus un identifiant unique désignant le message dans la base locale. Ainsi lorsque l'utilisateur souhaite lire un message, le thread principal accède à la base des mails.

    De manière générale, le téléchargement se fait en arrière-plan et il y a d'autres fonctionnalités qui sont susceptibles d'accéder à la base des emails via le thread principal. Il est donc impératif que le thread de téléchargement (backgroundworker en l'occurence) n'utilise pas de connexion SQLite, seul le thread principal doit pouvoir effectuer ce travail afin de garantir l'atomicité des opérations.

    Cordialement

    mardi 27 mars 2012 11:08
  • Bonjour,

    OK, dans ce cas pourquoi ne pas afficher à chaque "workerProgress", le contenu du nouveau mail dans la ListView. Normalement, cela sera fera de manière "instantannée" et vous n'aurez aucun freeze du Thread d'affichage.

    Le "runworker" s'occupera quand à lui de récupérer et sauvegarder les e-mails.

    Cordialement


    Gilles TOURREAU - MVP C#
    Architecte logiciel/Consultant/Formateur Freelance
    Blog : http://gilles.tourreau.fr
    - MCPD : Enterprise Developper / Windows Developper 3.5 / ASP .NET 3.5/4.0
    - MCITP : SQL Server 2008 Developper
    - MCTS : ADO .NET 3.5 / SQL Server 2008 Developper / Windows Forms 3.5 / ASP .NET 3.5/4.0

    mardi 27 mars 2012 11:24
    Modérateur
  • D'accord mais il faut bien que les mails soient sauvegardés sur le disque pour pouvoir être affichés.

    Pour des grosses boîtes mails (10K ou 20k mails), garder en mémoire tous les mails complets (ça peut-être très lourd avec les pièces jointes) risque de consommer énormément de mémoire, non ?

    L'avantage de sauvegarder au fur et à mesure est de libérer la mémoire allouée aux messages correspondants.

    mardi 27 mars 2012 11:28
  • juste pour répondre au problème de buffer

           Dim max = 400
            Dim oQueueBuff As New Queue(max)
    
            ' Peuple la colection sans conso
            For i As Integer = 0 To 100
                oQueueBuff.Enqueue("Object " & i)
            Next
    
            ' Lecture et consomme le fisrt (l'item zero)
            For i As Integer = 0 To oQueueBuff.Count - 1
    
                Console.WriteLine(oQueueBuff.Dequeue)
                MsgBox(oQueueBuff.Count)
            Next

    dans la deuxieme boucle on vois bien que les items sont supprimes au fur et a mesure ...


    Cordialement,
    Xavier TALOUR
    Alias Troxsa SendMail
    Voir le profil de Xavier TALOUR sur LinkedIn

    mardi 27 mars 2012 12:07
    Auteur de réponse
  • Quel intérêt en performance par rapport au buffer que j'avais mis avant ?

    A quel moment vider la queue ?

    mardi 27 mars 2012 12:36
  • Par ailleurs, encore une fois il faudra choisir la taille max de la file.
    mardi 27 mars 2012 12:41
  • Ce n'est pas une obligation de choisir la taille de la queue puis quelle traite directement les entrés sortie d'elle même

    "DeQueue" "De" comme delete va ce charger de supprimer automatiquement le premier items qui ce trouve dans la Queue, et forcement il va réadapter le nombre d'items de la collection. exemple l'item 1 deviens l'item 0, l'item 2 deviens l'item 1 et forcement le dernier est vide quand le traitement de l'item 0 est fini, si la collection comporte 5 items et que trois sont traités c'est l'item 4 et 5 qui auront comme nouvelle entrer 1 et 2 et donc les items 3, 4, 5 seront vide.

    L'avantage d'avoir une gestion de suppression directement dans la class c'est a ne pas avoir a programmer la gestion et le comportement de celui-ci.

    La taille ou la définition du buffer est pour moi inévitable a vous de faire la gestion de la mémoire ou de faire une norme de taille du buffer par taille mémoire physique !


    Cordialement,
    Xavier TALOUR
    Alias Troxsa SendMail
    Voir le profil de Xavier TALOUR sur LinkedIn

    mardi 27 mars 2012 13:19
    Auteur de réponse
  • Ce que je voulais dire par "à quel moment vider la queue" c'est à quel moment appeler deQueue.

    Pour reprendre votre exemple de code :

    Si je met ceci dans le runworker :

    ' Peuple la colection sans conso
            For i As Integer = 0 To 100
                oQueueBuff.Enqueue("Object " & i)
            Next

    A quel moment dois-je appeler ceci ?

    ' Lecture et consomme le fisrt (l'item zero)
            For i As Integer = 0 To oQueueBuff.Count - 1
    
                Console.WriteLine(oQueueBuff.Dequeue)
                MsgBox(oQueueBuff.Count)
            Next
    
    

    A chaque fois que la queue est pleine ?

    Dans ce cas au final ça revient au même qu'utiliser une liste.

    mardi 27 mars 2012 13:26
  • D'accord mais il faut bien que les mails soient sauvegardés sur le disque pour pouvoir être affichés.

    Pour des grosses boîtes mails (10K ou 20k mails), garder en mémoire tous les mails complets (ça peut-être très lourd avec les pièces jointes) risque de consommer énormément de mémoire, non ?

    L'avantage de sauvegarder au fur et à mesure est de libérer la mémoire allouée aux messages correspondants.

    Bonjour,

    Dans ce cas, dans la méthode "runworker", sauvegardez vos mails et remplissez une liste de chaîne de caractères qui va contenir uniquement les informations que vous voulez afficher. Dans la méthode "workerProgress" vous avez juste à afficher dans la liste le contenu de cette liste.

    Cordialement


    Gilles TOURREAU - MVP C#
    Architecte logiciel/Consultant/Formateur Freelance
    Blog : http://gilles.tourreau.fr
    - MCPD : Enterprise Developper / Windows Developper 3.5 / ASP .NET 3.5/4.0
    - MCITP : SQL Server 2008 Developper
    - MCTS : ADO .NET 3.5 / SQL Server 2008 Developper / Windows Forms 3.5 / ASP .NET 3.5/4.0

    mercredi 28 mars 2012 06:44
    Modérateur
  • Bonjour,

    Je ne suis pas sûr de bien comprendre votre suggestion.

    Pour les raisons citées plus haut je ne peux pas sauvegarder les mails depuis un thread différent du thread principal.

    C'est pour cela que je suis venu chercher ici la meilleure façon de mettre en place un système type producteur/buffer/consommateur, sachant que le téléchargement et la sauvegarde on des vitesses complètement variables dans les deux sens d'un environnement à l'autre et que sauvegarder les mails un par un est complètement inefficace.

    Cordialement

    mercredi 28 mars 2012 07:29
  • Bonjour,

    J'arrive un peu comme un cheveu sur la soupe ;-) je ne connais pas SQL Lite mais http://sqlite.org/threadsafe.html laisse pourtant supposer quelques possibilités.

    Egalement si on utilise deux connections séparées (une depuis l'UI, une créée et libérée depuis les traitements de fond) je dirais que les mécanismes habituels d'une base de données devraient s'appliquer et que du point la vue de la base ce ne sont que deux accès concurrents séparés (mais je suis d'accord que ce serait dommage d'insérer les données d'un côté pour les relire de l'autre).

    Pour le backgroundworker, une autre approche pourrait être d'envoyer un "reportprogress" si plus de x secondes se sont écoulées depuis le dernier et/ou si m messages ont été récupérés. De cette façon, l'interface est mise à jour aussi rapidement que possible (à chaque message si c'est lent ou par paquets de plusieurs messages si on a une connexion plus rapide).

    Je ne sais pas d'où sont récupérer les messages (ce sont des mails POP3 ?) mais une approche également assez fréquente est de récupérer les en-têtes et de récupérer le contenu en tâche de fond quitte à récupérer un contenu de façon anticipé à la demande si l'utilisateur clique sur ce message (en gros c'est un client de messagerie ?).

    Rien de bien précis mais peut-être qq pistes qui pourraient aider...


    Please always mark whatever response solved your issue so that the thread is properly marked as "Answered".

    mercredi 28 mars 2012 11:48
    Modérateur
  • Bonjour,

    Oui c'est effectivement un petit client mail.

    A propos de SQLite et le multi-thread, j'ai effectivement pris un raccourci en disant dans mon premier post que ça n'est pas supporté. C'est juste que la façon dont est traité le multi-threading ne me convient pas dans ce projet car lorsque un thread accède à une base SQLite il la verrouille et tout accès simultané depuis un autre thread (même à une table différente) lève une exception. Etant donné que le téléchargement des emails peut tout à fait se faire en tâche de fond pendant que l'utilisateur utilise d'autres services il faudrait que chaque connexion de mon application soit encapsulée dans une boucle dans l'attente d'un intervale d'ouverture de la base ce qui risque de prendre beaucoup trop longtemps si les emails se téléchargent très vite...

    Vos deux dernière suggestions sont très intéressantes, notamment l'idée du temps depuis le dernier message qui permettrait à mon code de ne pas trop reposer sur la capacité du buffer. Je vais essayer.

    En revanche télécharger séparément entêtes et contenus risque de représenter un peu trop de travail dans le temps que j'ai mais merci quand même.

    Cordialement

    mercredi 28 mars 2012 12:12