torsdag 14. juli 2011

WPF, Notifications and Parallellism

I'm working on getting our GUI responsive, which can be quite a challenge. We fetch a pdf or tiff over a wcf-service from the test-server running on a vm, rendering the first page and showing this to the user. We also have a strict policy on separating as much as possible into the domain-layer, and the domain-layer can not have any gui-classes. Fetching and rendering is done in the domain-layer, and the GUI is notified through the INotifiyPropertyChanged-mechanisms.
I've had several problems with the ownership of wpf-objects when a task in the domain-level sets a property which the gui subscribes to, and the gui then tries to redraw some metainformation in an overlay canvas. I've solved several situations by using TPL's ContinueWith-mechanism where the new task is run on the GUI-thread.
After working on a problem today, where images are fetched every time the user changes the selected row in a table, I started thinking if there is a better way to do this. If I could only get all notifications to run on the GUI-thread, without having to use TPL's ContinueWith...
So I've now created a small test-project that has some classes, a window showing info, and some extension methods that run all the notifications on the GUI-thread.
This is the essence of the extension methods:
public static TaskScheduler GuiContext;

public static void Notify(this PropertyChangedEventHandler self, INotifyPropertyChanged sender, string propertyName) {
if (self == null) return;
Task.Factory.StartNew(() => self(sender, new PropertyChangedEventArgs(propertyName)), CancellationToken.None, TaskCreationOptions.None, GuiContext);
}
The GuiContext field needs to be set on startup from the MainWindow, e.g. in the Loaded-event. It appears as though the extension-methods works like a charm.
I made the test-app start several threads that updates the objects in the controller several times, and as long as I allow the gui some cpu-time(put in some small sleeps in the threads), the gui is responsive and it updates the textboxes with values from the different threads.
I'm aware that just updating the same values from several threads is not a good idea, but for showing data in the gui I'm really just interested in showing the latest values, and I have control over cancellelation on the background tasks in the real application. So for the purpose of the test-app I don't think this is important.

I'm writing this post in the hopes that someone will take a poke at my project and tell me if they find any flaws or large weaknesses that I've overlooked. Here is a link to the project file.

torsdag 10. februar 2011

Memory leaks while converting from bitmap to bitmapsource

One project I'm working on is using TwainDotNet. It has been some work to get to work "exactly" like we want it to, but it's overall been a nice library to play with.
But today, I've been working on a memory leak... These bugs are really annoying to work on, but anyhow here is what I found out:
The problem is that the sample code and samples I've found on the internet for converting Bitmap to BitmapSource. The code usually looks something like this:
    image1.Source = Imaging.CreateBitmapSourceFromHBitmap(
        new System.Drawing.Bitmap(args.Image).GetHbitmap(),
        IntPtr.Zero,
        Int32Rect.Empty,
        BitmapSizeOptions.FromEmptyOptions());

This works fine, but it also creates a HBitmap in unmanaged memory that it doesn't free.
A better way to solve this is to store the HBitmap in a variable and call DeleteObject on it. DeleteObject is a function from gdi32.dll:
    [System.Runtime.InteropServices.DllImport("gdi32.dll")]
    public static extern bool DeleteObject(IntPtr hObject);

Here is the code I ended up with in our production code:
    private void TwainOnTransferImage(Object sender, TransferImageEventArgs e) {
        if (e.Image != null) {
            var bmp = new System.Drawing.Bitmap(e.Image);
            IntPtr hBitmap = bmp.GetHbitmap();
            image1.BeginInit();
            image1.Source = Imaging.CreateBitmapSourceFromHBitmap(
                hBitmap,
                IntPtr.Zero,
                Int32Rect.Empty,
                BitmapSizeOptions.FromEmptyOptions());
            image1.EndInit();
            image1.Source.Freeze();
            DeleteObject(hBitmap);
            bmp.Dispose();
            e.Image.Dispose();
        }
    }

søndag 19. september 2010

Spotify Playlist Folders

Eller kanskje egentlig: koffor har eg tømt Swing-spelelista mi på Spotify.
Spotify kom med ein ny funksjon for litt siden, "Playlist folders", tanken her er at du lager ein folder der du legger inn andre spelelister og får slik hierarkiske spelelister. Så istedetfor å ha ei speleliste som inneholder all swingmusikken min har eg nå fleire, og desse ligger i ein folder. Og vil eg høre på all swingmusikken så velger eg folderen som nåværende speleliste og får slik ein grei variasjon i ka swingmusikk som speles. I tillegg kan eg då ha spelelister som eg kan bruke når eg ska undervise, ei med sakte swing musikk, ei med rask, osb. :) I tillegg har eg endra navn på den gamle lista mi te å nå vere "Swing forslag" i håp om at nogen vil legge inn sangforslag der til meg, som eg igjen kan flytte over i dei spelelistene der eg synes dei passer. ;)

Oppdelingen eg nå har gjort blei for det meste gjort på bussen(offline lister) til Karmøy, og eg hørte for d meste bare kjapt på sangene før eg flytta dei, så eg rekner med at det blir litt meir rydding etterkvart. ;)

Her er linker til spelelistene mine slik eg har laga dei pr.nå:
Lister eg kommer te å bruke på kurs:
Swing SakteSwing Mellomtempo, Swing Rask, Swing Diverse, Hip-Hop-Swing, Blues
Andre lister:
For rask, Sakte ikkje kurs, Mellomtempo ikkje kurs, Rask ikkje kurs, Lindy Sakte, Lindy Mellomtempo, Lindy Rask, Charleston

Og eg har som sagt ei liste der eg håper folk vil legge inn forslag til meg: Swing forslag ;)

onsdag 21. juli 2010

ConcurrentQueueWithWait

For the time beeing, I'm working on a rather heavy computing problem at work. A normal case scenario has a computing time of about 1 hour, and a not unlikely, but bad scenario, has a computing time of about 7 hours. One "problem" is that I'm already using a pretty powerful computer:
  • Core i7 930 @ 2.80GHz (WEP 7.5)
  • 2 striped Intel 160GB X25M 2.gen Disks (WEP 7.9)
  • 12GB RAM (Operations WEP 7.5)
I've created several producers and consumers, and though I've done a lot of code optimizing I've got more to go on. My main clog on performance was the SQLite connection. SQLite is an embedded single-connection database, and I have some constraints on the interpretation of the data that forces me to write data more or less sequentially. This limits me too using one thread to execute the SQLiteCommands, and using the phx-lib, the SQLiteCommands is prepared only when execution is called. Preparing the commands is quite time consuming, so I've limited the queue of commands to avoid using more time than necessary on preparations. When a command is prepared I can change the values of the parameters without having to do the complete preparation step over again. I use the ObjectPool to generate and store the SQLiteCommands.
Have you ever worked with the Producer/Consumer pattern on several threads? If you have your code might haved looked something like this (pseudoocode):
Producer 1 and 2 (2 separate threads that I create several of)
while (work to do){
  Do some calculations on obj
  Lock (FirstLevelQueue(s)) { // one queue for both or 1 queue for each
    FirstLevelQueue(s).Enqueue(obj)
  }
}
Producer 3 and 4, that Consumes 1 and 2's queue(s)(2 separate threads)
while (producer 1 and 2 not done){
  dequeued = true
  while (dequeued) {
    Lock (FirstLevelQueue(s)) { dequeued = FirstLevelQueue(s).Dequeue(obj) }
    if (dequeued) {
      Do some work on obj
      while (SecondLevelQueue(s).Count > sizeLimit) Thread.Sleep(100)
      Lock (SecondLevelQueue(s)) { SecondLevelQueue(s).Enqueue(obj) }
    } else if (some complex criterion) {
      Thread.Sleep(100) 
    }
  }
}
Consumer (5. thread)
while (producers 3 and 4 not done) {
  dequeued = true
  while (dequeued) {
    Lock (SecondLevelQueue(s)) { dequeued = SecondLevelQueue(s).Dequeue(obj) }
    if (dequeued) {
      Do some work
    } else if (some complex criterion) {
      Thread.Sleep(100) 
    }
  }
}
I feel this code has a few weak points. First of all, locking is newer good since it takes time and it hinders consumers to dequeue for as long as anyone is queueing. Also in this code there is going to be a lot locks since all access uses this mechanism. Locking can also cause huge problems if you forget one or end up locking for too long, e.g. while sleeping. Second, the sleep statements are going to take a lot of time. In the code, on which I am working, I discovered that even a sleep as short as 10ms was way too long. Furthermore, since the sleep is so short the threads are going to wake up and consume quite a bit of resources while checking if there is any more work to do. Using .Net 4.0 and TPL(Task Parallell Library) you get a somewhat better and nicer code by using the ConcurrentQueue, which is thread-safe:
Producer 1 and 2 (2 separate tasks that I create several of)
while (work to do){
  Do some calculations on obj
  FirstLevelConcurrentQueue(s).Enqueue(obj) // one queue for both or 1 queue for each
}

Producer 3 and 4, that Consumes 1 and 2's queue(s)(2 separate tasks)
while (producer 1 and 2 not done){
  dequeued = true
  while (dequeued) {
    dequeued = FirstLevelConcurrentQueue(s).TryDequeue(obj)
    if (dequeued) {
      Do some work on obj
      while (SecondLevelConcurrentQueue(s).Count > sizeLimit) Thread.Sleep(100)
      SecondLevelConcurrentQueue(s).Enqueue(obj)
    } else if (some complex criterion) {
      Thread.Sleep(100) 
    }
  }
}

Consumer(5. task)
while (producers 3 and 4 not done) {
  dequeued = true
  while (dequeued) {
    dequeued = SecondLevelConcurrentQueue(s).TryDequeue(obj)
    if (dequeued) {
      Do some work
    } else if (some complex criterion) {      Thread.Sleep(100) 
    }
  }
}
Now, this code looks a lot better since I'm rid of the locks. This reduces some common error points and the consumers do not stop the producers from enqueueing. But there is still a lot of sleep statements, and the threads often wake up to check if there is more work to do, and as stated, for my part, even a 10ms sleep were to long.
I decided that I really wanted code that looked somewhat more like this:
Producer 1 and 2 (2 separate tasks that I create several of)
while (work to do){
  Do some calculations on obj
  FirstLevelConcurrentQueue(s).EnqueueWithEvent(obj) // one queue for both or 1 queue for each
}

Producer 3 and 4, that Consumes 1 and 2's queue(s)(2 separate tasks)
while (producer 1 and 2 not done){
  FirstLevelConcurrentQueue(s).TryWaitUntilDequeued(obj)
  Do some work on obj
  SecondLevelConcurrentQueue(s).EnqueueUpToLimit(obj)
}

Consumer(5. task)
while (producers 3 and 4 not done) {
  SecondLevelConcurrentQueue(s).TryWaitUntilDequeued(obj)
  Do some work
}
In my mind, this code is shorter and way more readable. In addition, the threads do not wake up all the time to check if there is some work to do. This actually makes the code more cpu efficient and gives you more cpu resources to do your other computing.
It didn't take me much googling to discover the AutoResetEvent, an easy-to-use, thread-safe event mechanism. Using the AutoResetEvent and the ConcurrentQueue I implemented a class I called ConcurrentQueueWithWait. I included this in the dll for ParallellExtensionsExtras, which we use at work, and after some tweaking and testing I ended up with this class:
public class ConcurrentQueueWithWait : ConcurrentQueue {
  private AutoResetEvent _enqueuedEvent = new AutoResetEvent(false);
  private AutoResetEvent _dequeuedEvent = new AutoResetEvent(false);

  /// The size limit of the ConcurrentQueue, if you use EnqueueUpToLimit for enqueueing
  public int QueueLimit { get; set; }

  /// The timeout for Dequeue-events, and EnqueueUpToLimit-events
  public int TimeOut { get { return _timeOut; } set { _timeOut = value; } }
  private int _timeOut = 5000;

  /// Sets both the Enqueue and Dequeue events twice to try to avoid any deadlocks
  public void ProducerFinished() {
    _enqueuedEvent.Set();
    _dequeuedEvent.Set();
    Thread.Sleep(10);
    _enqueuedEvent.Set();
    _dequeuedEvent.Set();
  }

  /// Enqueues the item if the queue limit is not reached. If limit is reached it will wait until an item is 
  /// dequeued, and then enqueue.
  /// Note: on a timeout it will enqueue wether or not the limit has been reached.
  public void EnqueueUpToLimit(T item) {
    if (QueueLimit > 0 && base.Count > QueueLimit)
      _dequeuedEvent.WaitOne(TimeOut);
    base.Enqueue(item);
    _enqueuedEvent.Set();
  }

  /// Adds an object to the end of the ConcurrentQueue, and sends an event to anyone waiting to dequeue.
  public void EnqueueWithEvent(T item) {
    base.Enqueue(item);
    _enqueuedEvent.Set();
  }

  /// Attempts to remove and return the object at the beginning of the queue. If none is found it will wait for 
  /// an enqueued signal, or timeout.
  /// Should be used as a regular TryDequeue, but without the while(...) Thread.Sleep(xx)
  /// Returns wether or not a dequeue was successful
  public bool TryWaitUntilDequeued(out T result) {
    bool res = true;
    if (base.IsEmpty)
      res = _enqueuedEvent.WaitOne(TimeOut);
    if (!res) {
      result = default(T);
      return false;
    }
    res = TryDequeue(out result);
    _dequeuedEvent.Set(); //I do not need to check result here, since a false should equal an empty queue 
                                        //and therefore any pending enqueues should be fired
    return res;
  }

  /// Attempts to return the object at the beginning of the queue. If none is found it will wait for an 
  /// enqueued signal, or timeout.
  /// Should be used as a regular TryPeek, but without the while(...) Thread.Sleep(xx)
  /// Returns wether or not a peek was successful
  public bool TryWaitUntilPeeked(out T result) {
    bool res = true;
    if (base.IsEmpty)
      res = _enqueuedEvent.WaitOne(TimeOut);
    if (!res) {
      result = default(T);
      return false;
    }
    res = TryPeek(out result);
    return res;
  }
}
I had to put in a timeout to avoid deadlocks both on enqueue and dequeue. In tests, which I've run on a small test-set(2 seconds run-time), I get the extra 5 seconds in about 1 out of 6 runthroughs, and I figure that 5 seconds extra don't matter much with run-times normally from 20 minutes and up.

This class meant that I ended up with this implementation of my tasks:
Producer 1 and 2 (2 separate tasks that I create several of)
while (work to do){
  Do some calculations on obj
  FirstLevelConcurrentQueue(s).EnqueueWithEvent(obj) // one queue for both or 1 queue for each
}

Producer 3 and 4, that Consumes 1 and 2's queue(s)(2 separate tasks)
while (producer 1 and 2 not done){
  while (FirstLevelConcurrentQueue(s).TryWaitUntilDequeued(obj)) {
    Do some work on obj
    SecondLevelConcurrentQueue(s).EnqueueUpToLimit(obj)
  }
}

Consumer(5th task)
while (producers 3 and 4 not done) {
  while (SecondLevelConcurrentQueue(s).TryWaitUntilDequeued(obj)) {
    Do some work
  }
}
Not quite the easy and nice code I was hoping for, but really close enough to make me happy. My threads now only come out of sleep every 5 seconds or when there actually is some work to do.
Some minor problems with this implementation:
  • If I get a timeout on EnqueueUpToLimit I've set it to just enqueue anyway; so the limit I set on the queue is not absolute. My tests show that this is not a problem for my program, so I'm pleased with it.
  • I do get the timeout as an extra runtime in about 1 out of 6 times.
  • I still have to use the try-pattern.
  • If a bug finishes the 5th task before the 3rd and 4th, my program gets an extreme runtime since the 3rd and 4th constantly waits for the enqueue-timeout. This appeared to be a Heisenbug when I encountered it.
  • There is still some plumbing code needed, but way less than before.

måndag 5. april 2010

Herlig påske og "ministoltzen"

Då nærmer ein ganske så herlig påske seg slutten... Eg har fått spelt 1,5 dag med Army of two 40th day i lag m venner, eit par-tre dager med Dante's Inferno, eg fullførte siste. Og eg har til og med klart å få gjort ein del fornuftig i tillegg:
Rydda i ein haug m papirer, deriblant kikka gjennom sjølvangivelsen.
Gjort unna litt småting i leiligheten.
Fått lagt ut nogen ting me ikkje trenger lenger på Finn.no og faktisk fått solgt 1 ting allerede første dagen.
Rydda begge bodene, d e virkelig mye bedre når bodene e halvfulle i bredden ut frå alle veggene, istedetfor halvfulle i høyden. Før og etter bilder frå boden oppe:

I dag var me ute og gjekk tur, noge som me har fått gjort eit par-tre-fire gonger eller noge sånt i påsken, og i dag oppdaga me ei ny favorittrute. "Ministoltzen", ei rute som går ganske bra oppøve rett frå over veien. Me må gå opp på asfaltert vei te rett ovenfor Frode og Marianne, og videre derifrå kommer me over på ein natursti som e ganske bratt te å begynne med. Me gjekk eit stykke oppøve og bestemte oss for at der skulle me ha endepunktet på d som blir vår "vanlige" trimtur nå. Turen blir noge kortare og mindre bratt enn Stoltzen, men eg satser på at me kommer oss opp den løypa mye oftare enn Stoltzen, siden turen opp Stoltzen innebærer ein bortimot 7km sykkeltur kvar vei.
Utsikt frå Ministoltzen:

søndag 7. mars 2010

Helvetesøvelsen(Dans)

Eg fekk ein mail frå Per for altfor lenge siden(3 veker) med sps om eg kunne sende han ein beskrivelse av helvetesøvelsen. Eg hadde bare ein kortvariant liggande men her ska eg prøve å skrive ein skikkelig beskrivelse av den.

Helvetesøvelse

I denne øvelsen skal G føre J i kryssteg/crossovers. G vil føre stega i forskjellig tempo, lengde, framover/bakover og føre spesielle figurar som bryter opp desse stega.
Dette er ein føringsøvelse som trener hovudsaklig ramme. Til å begynne med vil den ende opp med å trene opp noget stive armer, men etter kvart som ein blir meir avansert skal ein stå mest mulig i egenbalanse, og slappe mest mulig av mellom punktene der sjølve føringa skjer. Slik at ein trener kor lang tid det tar følger å reagere når fører begynner å føre. I tillegg gjer det fører bevisst på at dei faktisk må følgje med på kor følger er i sine bevegelsar når dei fører.
Ein kraftig forenkling av denne er at J lener seg litt mot G. Siden dei då automagisk må spenne opp armane for å ikkje falle på G.
Delen som går på å bryte ramme trener reaksjonsevnen, samt at den prøver å få folk frå å bruke denne øvelsen til å trene seg inn i fastlåste mønstre.

Variasjoner som kan gjeres i lukka

    * Crossovers tempo og lengde
    * Lukka med spinn
    * Til musikken – kan taes samtidig i åpen og lukka
    * Legge inn i dansen(veldig korte og kjappe steg) – kan taes samtidig i åpen og lukka
    * Halve steg
    * Bakover, husk halve òg her – mye d samme om denne taes i åpen eller lukka

Variasjoner som kan gjeres i åpen

    * Crossovers tempo og lengde
    * Til musikken – kan taes samtidig i åpen og lukka
    * Legge inn i dansen(veldig korte og kjappe steg) – kan taes samtidig i åpen og lukka
    * Halve steg
    * Bakover, husk halve òg her – mye d samme om denne taes i åpen eller lukka

Variasjoner som bryter ramme

    * Lukka: Få foten te å ”sveve” i lufta
    * Åpen/Lukka: Få til J til å ”sveve” og krysse bak G kne. Kan i lukka brukes som inngang til Sweep the floor
    * Lukka: Bakover til J ned
    * Lukka: Ned til sveip(J har foten i lufta bakover, G går rundt)
    * Åpen: Ned til J foten ut direkte bakover. Føring m tilbake og ut. Kan òg taes videre til sveip.

torsdag 25. februar 2010

Tide busser i utide

Med alt snøfallet i Bergen som nå har vart så altfor lenge så har eg nå ein periode prøvd å ta bussen te og frå jobben. Eg tok den ein kort periode før jul, og kjøpte meg eit månedskort etter jul. Når månedskortet gjekk ut så begynte eg å lure på kva alternativer som fantes te buss... 20min til jobb(buss+gange) gjekk forsåvidt greit, men det forutsatte at eg tok ein arbeidsrute-bussane som går til Haukeland. Desse går 0457, 0625, 0626, 0655 og 0725, som i lengden blir litt småtidlig. Dei var som regel ikkje meir enn 2-3min for tidlig eller mye over 5min for sein, så det å stå ute i eit drygt kvarter på det verste var vel kanskje det verste med den bussen.
Så var det him igjen då... Å gå tar 55min med snøen som ligger for tida. Den kortaste tida eg har klart med buss kombinert med gange er 40min, vanlig tid var sannsynligvis 1t10min. Dei lengste tok vel noge sånt som 1t45min... Dei to gongene som virkelig gjorde meg forbanna var fredag 8/1 der bussen som skulle ha starta å kjøre frå Bystasjonen kl 1740 ikkje dukka opp. 1745 fekk eg vite frå ei av dei andre som stod og venta på samme buss at ei venninne av henne hadde gått på bussen me venta på i Olav Kyrresgt. Venninna gjekk fram og spurte busssjåføren om koffor han ikkje hadde vore innom Bystasjonen, og fekk til svar at han hadde droppa det stoppet fordi han ikkje hadde hatt tid. Eg tok ein taxi den gongen og fekk igjen penger, mandag 1/2 derimot kom heller ikkje bussen til bystasjonen, eg meiner det var i 17-tida eg venta på den dagen òg. Den gongen stod vel eg og 15-20 andre og venta på den bussen som aldri dukka opp. I kundegarantien til Tide står det at du får dekka taxi om du blir 20min forsinka, når er du 20min forsinka? Er det når neste buss kommer 20min etter den du egentlig venta på, eller må du vente ein ekstra 5min for å sjå om denne dukker opp? Ei av dei som skulle tatt den bussen endte opp med å ta neste fordi det var ingen ledige taxier på bystasjonen mens me venta på neste buss. Eg må virkelig sei at det frister utrolig lite å ta buss når du ikkje eingong kan stole på at dei kommer til det bussstoppet dei skal starte å kjøre frå.


Det eg nå har endt opp med er å kjøpe meg piggdekk til sykkelen og sykle til jobb. Eg kan då for det meste sykle på fortauene eller sykkelstiane, men må bruke veien ein del. Har det nylig komt snø, så kan eg derimot bare glemme å sykle nogen aen plass enn på veien. Men, men eg bruker nå bare 20min til og frå jobb. Så eg sparer opptil eit par timer kvar dag. Litt fakta: sykkelturen e ca 4,3km, og eg har klart den på 10min på tørt føre med min gode sykkel.

Men nok om det å ta buss til jobb, det å ta buss generelt sett her frå Laksevåg er det jo massevis ein kan sei om. Dei kommer sjelden på den tida dei skal komme, og forundrande ofte så kommer dei 2 bussar samtidig, den på rute 70 som skulle gått for 10min siden og den rute 70 som skal gå nå, evnt rute 71 på begge eller ein av dei i tillegg til dei 2 70'ane.
Så var det rutetabellen frå Laksevåg, man-fre (-A: arbeidsrute, -7 rute 70, -1 rute 71):
Kl 04: 57-A   = Klokka 04:57 Arbeidsruta
Kl 05: 59-7
Kl 06: 13-7, 25-A, 30-7, 10-1, 26-A(1), 27-7, 55-A, 45-1, 57-7 (ja, dei står sånn i ruta frå Laksevåg/Fergeveien)
Kl 07: 10-1, 32-7, 26-A, 25-7, 37-1, 55-7, 52-7
Kl 08: 07-1, 25-7, 20-1, 37-7, 40-1, 50-1
Kl 09: 00-7,       10-1, 22-7, 40-1
Kl 10: 00-7, 00-1, 10-1, 22-7, 40-1, 55-1
Kl 11: 00-7,       10-1, 22-7, 40-1
Kl 12: 00-7,       10-1, 22-7, 40-1, 55-1
Kl 13: 00-7,       10-1, 22-7, 40-1, 55-A
Kl 14: 07-1, 08-1(1),    22-7, 38-1
Kl 15: 00-7, 05-A, 02-1, 08-1, 30-7, 38-1, 52-1
Kl 16: 00-7, 08-1, 30-7, 38-1
Kl 17: 02-1, 08-1, 30-7, 27-1, 38-1
Kl 18:                10-1,       40-7
Kl 19: 00-7,       10-1,       42-7
Kl 20: 00-7,       10-1,       42-7
Kl 21:                10-1, 27-7
Kl 22:                10-1, 50-7
Kl 23: 20-1
Kl 00: 20-1
Wow, når eg nå skreiv ned alle tidene så fant eg faktisk system i tidene på deler av dagen... Såklart dei delene av dagen det absolitt minst sannsynlig at eg tar buss, men dog... Eg liker spesielt å finne 2punkter(1) der faktisk samme ruten går med 1 minutts mellomrom. Og det er jo virkelig dei to tidspunktene på dagen det virkelig er mye folk som tar dei bussane, sikkert spesielt 14.07 og 14.08...
Ellers så er det jo ein ganske vanlig trend her at 2 busser går innenfor 5 eller 10 minutt av kvarandre, og så er det 20-30minutt te neste... Og det er ca det samme him igjen... Gåturen frå byen tar drygt 30min, så er været greit og bussen ikkje kommer innen kort tid frå me kommer til bussstoppet så ender det ofte med at me går.

Nei, eg må virkelig sei at busstilbudet må forbedres kraftig om eg ska ta buss på noge fast basis. Første krav er at bussane kjører frå alle stoppa dei ska kjøre frå, andre at dei er så godt som i tide, og eg skulle gjerne hatt eit lett system å forholde meg til for når dei går. At det går ein buss kvart kvarter frå 3minutt over heil, mellom kl 6 og 22 er tider det er lett å forholde seg til.
Men uansett så lenge eg ferdes i Bergen og det ikkje ligger snø overalt så kommer eg nok te å bruke sykkelen, det går MYE fortare enn noge anna. Du kan sykle nesten frå dør til dør, har lite problemer med å finne parkering, parkeringa er gratis og du trenger ikkje å gå langt frå parkeringsplassen til der du skal.