Windows Phone Performance og billeder

Jeg har nu arbejdet med Windows Phone i lang tid og i den forbindelse fået en hel del erfaring med performance på Windows Phone… så jeg tænkte at jeg ville dele lidt erfaring med, dem af jer som til hverdag eller på hobby plan udvikler til Windows Phone.

I de fleste tilfælde er det faktisk super nemt at lave en app til Windows Phone, selvfølgelig meget afhængigt af ambitionsniveauet, men generelt set er det ikke så svært. Der er god hjælp fra Visual Studio + Blend og SDK’et er godt dokumenteret … og så er det masser af eksempler fra andre udviklere.
Et sted hvor dokumentationen er relativt ”stille” er ang. Billeder og hvordan billeder håndteres på Windows Phone. Der er nemlig noget så smart som en ”image bufferen” som skal sikre hurtig load af billeder fra hukommelsen… dokumentationen glemmer bare at nævne at ”image bufferen” ikke har en øvre grænse, den kan blive uendelig stor, altså i moderne tale har vi et potentielt ”memory leak” medmindre vi tænker os godt om som udviklere. Hvis vi, som udviklere, ikke gør noget aktivt fjernes billederne nemlig ikke fra hukommelsen før appen lukker ned og det er et STORT problem hvis vi fx vil håndtere mange billeder.

Så hvordan laver man en WP app. som kan vise 1 million billeder?
En Windows Phone app må på intet tidspunkt bruge mere end 90 MB hukommelse og skal helst holde sig under 70 MB. En helt tom applikation fylder ca. 8MB i hukommelsen: ca. 4.5MB til .NET Runtime det resterende bruges til UI’et pagen og tilhørende kontroller etc.
Det betyder at man rundt regnet har 60 MB hukommelse til at holde de 1 million billeder og tilhørende UI. Vi kan derfor hurtigt konkludere at alle billederne ikke kan være i hukommelsen på én gang… de skal loades dynamisk.

Dynamisk generering (Virtualizing i Windows Phone termer) er derfor den eneste udvej. For eksemplets skyld bruger vi en ”ListBox” control til at vise billederne. I praksis er det komplet urealistisk at skulle lave en ”scroll” gennem så mange billeder, men denne løsning er perfekt til at teste performance.
Sm standard loader en ListBox alt sit indhold, altså i dette tilfælde vil ListBox kontrollen forsøge at hente alle billederne og gemme dem i hukommelsen for at give den bedste scroll oplevelse. Selvsagt en dårlig ide. Denne standard opførsel kan dog hurtig ændres ved at ændre ItemTemplate på ListBox’en til et VirtualizingStackpanel, således:

<ListBox x:Name="MainListBox" Margin="0,0,-12,0" ItemsSource="{Binding Items}" SelectionChanged="MainListBox_SelectionChanged">
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <VirtualizingStackPanel />
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
                <ListBox.ItemTemplate>
                    <DataTemplate>
                      <StackPanel Margin="0,0,0,17" Height="400">
                            <Image Source="{Binding LineOne}"/>
                            <!--<TextBlock Text="{Binding LineOne}" TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
                          <TextBlock Text="{Binding LineTwo}" TextWrapping="Wrap" Margin="12,-6,12,0" Style="{StaticResource PhoneTextSubtleStyle}"/>-->
                      </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>

En hurtig test viser også ganske rigtigt at denne løsning er ganske effektiv og rent faktisk er i stand til at vise de mange billeder uden at bryde sammen. Men tester man lidt grundigere og laver en serie af hurtige ”scroll” og navigere en smule ”aggressivt” ser man hurtigt at dette ikke er en god nok løsning.
Altså så længe vi har nogle rolige og afslappede bruger skal det nok gå, men hurtige bevægelser betyder at hukommelsen hurtigt kommer over 90MB grænsen.
Hvis man er lidt fingernem på musen kan man i emulatorer komme op på mere end 200MB. Det er ikke de 200MB, i sig selv, der er interessante, det er det faktum at de aldrig bliver fjernet af GC’en. Altså vi har et memory leak på 200MB… se selv herunder og test meget gerne selv :)

(Bemærk de røde tal i højre margin, de skulle aldrig kommer op over 90)

Illustration: martin esmann
Illustration: martin esmann

Et eller andet sted i vores app er der et memory leak… men hvor og hvordan løser vi det? Efter en del research på nettet og gennem egne tests ser det ud til at problemet opstår fordi billederne hentes ”dynamisk” fra nettet. Det der tilsyneladende sker er at ListBox’en forespørg et billedelement, men inden billedet er hentet og klar til at blive vist for brugeren så har brugeren ”scroll’et” forbi og vores ”item container i ListBox” er blevet fjernet. Det betyder at billedelementet ikke længere et bundet til et ListItem… men så at sige hænger frit i hukommelsen. GC’en kan derfor ikke finde frem til og fjerne billedelementet. Deraf vores memory leak.

Jeg har forsøgt mig med rigtig mange løsninger og mange af dem har været ret kreative og omhandlet annullering af downlaod etc. Problemet kan dog tilsyneladende altid fremprovokeres så længe vi har en asynkron operation. Alt i Windows Phone er pr design asynkront, med undtagelse af IsolatedStorage (app filsystemet på Windows Phone).

Den eneste gyldige løsning jeg har kunne lave har derfor været at downloade billederne til IsolatedStorage og efterfølgende hente dem synkront til UI’et. Det er dog ikke helt så simplet som det lyder, så jeg vil foreslå at du selv kigger koden igennem … det ender med at være rigtig mange kodeliner.

Hvis du stresser appen kan den godt komme op på nogle af 80MB, den skulle dog gerne falde ned igen hurtigt.

Brug gerne koden i dine egen 1 million billeder app :)

Hent koden her: Source

PS. Jeg har ikke testet koden med 1million billeder, det er min tålmodighed simpelthen ikke til ;)

Kommentarer (4)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Daniel Madsen

Det betyder at billedelementet ikke længere et bundet til et ListItem… men så at sige hænger frit i hukommelsen. GC’en kan derfor ikke finde frem til og fjerne billedelementet. Deraf vores memory leak.

Det burde nu ikke være problemet, GC'ens opgave er netop at finalize objekter der så at sige "hænger frit i hukommelsen". Problemet er snarere at du stadig har referencer til disse objekter, omend de ikke nødvendigvis er så åbenlyse (f.eks. delegates eller event binding kan nogle gange holde referencer) - dette forhindrer GC'en i at frigive objekterne.

Billedobjekter især har normalt referencer til native resources og det er derfor vigtigt at du explicit kalder Dispose() på disse når du er færdig med at bruge dem.

Og btw. så er aggressiv brug af GC.Collect() normalt et symptom på "you're doing it wrong" snarere end løsningen :-)

Jeg synes iøvrigt det virker lidt mærkeligt at poste halvfærdig / fejlbehæftet kode her på blogging - for mig at se hører debugging måske snarere hjemme på StackOverflow? :)

  • 10
  • 0
Martin Esmann

Helt enig, det burde ikke være nødvendigt at køre GC'en selv... men prøv at køre sample appen. Så længe alt gøres langsomt og roligt er der ingen problemer, men er man for hurtig så kommer problemet.

Jeps, events og delegates kan give en del udfordringer, men i dette tilfælde er det altså ikke kode jeg selv har skrevet det er "drag & drop" koden som giver udfordringen og ikke slipper alle referencer... derfor er jeg nød til selv at kode det hele op.

Billedobjekter... igen i drag & drop koden kan jeg ikke manuelt kalde dispose... det skulle gerne initieres af Virtualizing delen... og det går fint så længe alt gøres langsomt. Stress udløser fejlen.

enig, manuel brug af GC er oftest ikke et godt tegn :) Men GC køre anderledes på WP end desktop og derfor kan det i nogle tilfælde være bedre at køre GC end at kommer mem. limiet... eller ;)

Stackoverflow er tiltænkt tekniske spørgsmål ikke nødvendigvis "sample" kode som denne.

Hvad mener du med at jeg ikke bør poste halvfærdig kode?! Viser koden ikke min pointe tilstrækkeligt?

  • 2
  • 0
Martin Esmann

... og så glemte jeg lige en lille ting. Når jeg laver en memory profiling af appen ser det ud til at "de frie" billedelementer bindes til app'en og derfor først fjernes når appen lukker helt ned. Det er lidt svært at se præcis om det korrekt jeg kan blot konstatere at jeg har en masse "Visuals" som ikke bliver fjernet.

  • 1
  • 0
Morten Jensen

PS. Jeg har ikke testet koden med 1million billeder, det er min tålmodighed simpelthen ikke til ;)

Hahahah jeg havde besluttet mig for at spørge dig hvordan du havde testet 1 mio billeder, og om hvordan filsystemet håndterede det - lige indtil jeg læste din sidste linie :)

Jeg udvikler normalt mod *nix og/eller embeddede targets, men jeg synes din artikel er spændende. Jeg synes det er HELT i orden at poste kode der ikke er færdigt. Det er meget bedre end at gemme på sit hyggeprojekt fordi man er bange for folks gransken.

  • 1
  • 0
Log ind eller Opret konto for at kommentere