poniedziałek, 26 stycznia 2015

Warsztaty o programowaniu wielowątkowym z Javy ale w Scali (chyba część pierwsza)

Poniżej materiał do warsztatów uczących używania mechanizmów wielowątkowych dostępnych w Javie aleee programować będziemy w Scali. Kiedy się odbędzie warsztat? Warsztat odbędzie się w momencie jego dobycia. Pewnie w przyszłym miesiącu.

Celem warsztaty będzie zapoznanie uczestników (którzy w założeniu nie stykają się z problemami wielowątkowości na co dzień) z ewolucją którą przeszła Java do wersji 1.7 - (java.util.concurrent, fork-join i takie tam.) Jednocześnie nie zarzucając słuchaczy zbyt wielką listą szczegółów.

Aby upiec dwie pieczenie na jednym ruszcie - będziemy przy okazji uczyć się Scali. To trochę jak nauka angielskiego poprzez tłumaczenie najnowszego wydania "Świata Dorsza" na tenże język.

Na warsztat przygotuję jakieś próbki kodu tak aby uczestnicy nie musieli pisać wszystkiego od początku. Kod będzie kompletny np. w 70% i nauka odbywać będzie się poprzez uzupełnienie brakujących fragmentów z towarzyszącym niezwykle krytycznym procesem wyciągania wniosków.

Po co uczyć się "starych" mechanizmów wielowątkowości jeśli ostatnio różne nowe rozwiązania działają na wyższym poziomie abstrakcji? No po to aby wiedzieć dlaczego one działają na wyższym poziomie abstrakcji i dlaczego w ogóle działają.

W jakieś wielkie i wyczerpujące opisy w tym artykule się nie bawiłem bo wszystko omówimy sobie w trakcie warsztatu.

Pojedyncze wątki na start

object SingleThreadExample {

  def main(args: Array[String]) {
    val r=new Thread(new MyRunnable("Interfejs"))
    val t=new MyThread("Klasa")
    
    r.start()
    t.start()
    
    r.join() //czekamy na zakończenie obydwu watków
    t.join()
  }
  
  class MyThread(m:String) extends Thread {
      override def run=println(s"Jestem w watku : ${m}")
  }  
  
  class MyRunnable(m:String) extends Runnable {
    def run = println(s"Jestem w runnable : ${m}")
  }
}
Nauka :
  • Runnable to interfejs ale w Scali nie ma słowa kluczowego implements.
  • Kiedy damy s przed stringiem to można wygodnie wstawiać weń zmienne
  • Na końcu dajemy join aby główny wątek main nie skończył się przed wątkami badanymi

Kilka wątków dla współpracy

W tym przykładzie tworzymy kilka wątków, które współpracują ze sobą sumując wybrane elementy z tablicy i następnie wyliczają ostateczną sumę. Takie MapReduce dla ubogich. Z przykładu można się także nauczyć ciekawego sposobu inicjalizacji kolekcji poprzez "tabulate" a także trochę deklaratywnych transformacji tychże kolekcji.

A i w ScalaIDE działa już generowanie metod w traitach przy pomocy ctrl+1.

object CoupleThreeadsCooperation {

  def main(args: Array[String]) {
    val n=100
    val v=Vector.tabulate(n)(i=>i*2) //1
    
    val runnables=(0 to n/10).map(i=>new Summarizer(i*10,v)) //2
    val threads=runnables.map(new Thread(_)) //2
    threads.foreach(_.start()) //3
    threads.foreach(_.join)  //3
    
    val sum=runnables.map(_.result).sum //4
    println(sum)
  }
  
  class Summarizer(startIndex:Int,v:Vector[Int]) extends Runnable {
    
    private var _result:Int=0 // 5
    
    def run(): Unit = {
      val dataForThread=v.slice(startIndex, startIndex+10)
      println(s"summarizing elements ${startIndex} : ${startIndex+10} - ${Thread.currentThread().getName}") //6
      _result=dataForThread.sum
    }
    
    def result=_result //5
  }
}
  1. Używamy wektora bo tam wydajniej dostać się do danego elementu z indexem "i" także "slice" też pewnie działa szybciej
  2. Zamiast dziwnymi pętlami przetwarzamy dane lamdami.
  3. Podobnie można zażądać wywołania konkretnej metody an wszystkich elementach
  4. Przy tej linii w ramach odpowiedniego czasu i chęci słuchaczy można wspomnieć jak to jest zrobione, że dla kolekcji intów mamy metodę sum
  5. Jest stan i jest mutable :(
  6. Na potrzeby ćwiczenia walimy tekst zwykłym println

Kilka wątków - rywalizacja

Tym razem zobaczymy dlaczego programowanie wielowątkowe może nie działać. Uruchamiamy 100 wątków, każdy zwiększa stan licznika o 1- powinno być 100 - powinno a nie jest.

object CoupleThreadsWithGlobalState {

  private var global=0
  
  def main(args: Array[String]) {
    val workers=Seq.fill(100)(new Worker()) //1
    workers.foreach(_.start)
    workers.foreach(_.join)
    
    println(s"global na końcu ${global}")
  }
  
  class Worker extends Thread{
    override def run=global=global+1
  }
}
  1. "Seq" - tutaj tak przy okazji skąd się wzięło Seq - zerknijcie tutaj - Scala - Hierarchia kolekcji

Locki

object CoupleThreadsWithGlobalStateGuardedByLocks {

  private var global=0
  private val monitor=new Object() //1 pokazać przykład ze złym monitorem
  
  def main(args: Array[String]) {
    val workers=Seq.fill(100)(new Worker())
    workers.foreach(_.start)
    workers.foreach(_.join)
    
    println(s"global na końcu ${global}")
  }
  
  class Worker extends Thread{
    override def run=monitor.synchronized{  //1
    	  global=global+1 
      }
  }
}
  1. W scali nie ma słowa kluczowego synchronized. Jest za to metoda na AnyRef, która robi to samo

Atomic Integer

AtomicInteger wykorzystuje specjalne instrukcje procesora, który w jednym cyklu może wykonać dwie operacje w sposób atomowy. Procesory robi się z krzemu, a krzem uzyskuje chyba z piasku. W Afryce i Azji działają gangi, które nielegalnie przemycają piasek - ale wracając do przykładu zastosowanie AtomicInteger bardzo uprościło kod

object CoupleThreadsWithGlobalStateGuardedByLocks {

  private val global=new AtomicInteger(0)
  
  def main(args: Array[String]) {
    val workers=Seq.fill(100)(new Worker())
    workers.foreach(_.start)
    workers.foreach(_.join)
    
    println(s"global na końcu ${global}")
  }
  
  class Worker extends Thread{
    override def run=global.incrementAndGet()
  }
}

Producent konsument - kolejka

Tutaj trochę dłuższy przykład. Tym razem do trzech współpracujących wątków dodajemy kolejkę, na której blokują się wątki producenta i konsumenta w zależności czy kolejka jest pusta czy pełna.

object ProducerConsumerQueue {

  def main(args: Array[String]) {
    val q: BlockingQueue[Int] = new LinkedBlockingQueue(3) //1
    val p1 = new Thread(new Producer(q))
    val p2 = new Thread(new Producer(q))
    val c = new Thread(new Consumer(q))

    p1.start()
    c.start()
    p2.start()
  }

  class Producer(q: BlockingQueue[Int]) extends Runnable {

    private val r = new Random()

    @tailrec  //3
    final def run(): Unit = {
      val v = r.nextInt(10)
      q.put(v)       //2
      println(s"producing ${v} :  queue size ${q.size}")
      TimeUnit.SECONDS.sleep(3)
      run() //3
    }
  }

  class Consumer(q: BlockingQueue[Int]) extends Runnable {
    def run(): Unit = {
      while (true) {
        val v = q.take() //2
        println(s"consumed ${v} :  queue size ${q.size}")
        TimeUnit.SECONDS.sleep(2)
      }
    }
  }
}
  1. Kolejkę ustawiamy na 3 elementy aby się "zapchała" w trakcie pracy
  2. put i take blokują wątek w oczekiwaniu na rezultat
  3. A to taki eksperyment - teoretycznie zastąpiliśmy popularną pętlę "while(true)" rekurencją - chyba działa

Executory

W poniższym ćwiczeniu nie będziemy już tworzyć wątków z palca lecz użyjemy dodanych do Javy5 "Executorów" (lub po polsku Egzekutorów lub poprawnie po angielsku Executors)

object ExecutorsExample {

  def main(args: Array[String]) {
    val e=Executors.newFixedThreadPool(3) //1
    
    (1 to 100).foreach{i=>
      println(s"run task ${i}") //2
      e.submit(new Task())
    }
    e.shutdown()
  }
  
  class Task extends Runnable {
    def run(): Unit = {
      println(s" running thread ${Thread.currentThread().getName}") //2
      TimeUnit.SECONDS.sleep(2)
    }
  }
}
  1. Inicjalizujemy pulę złożoną z trzech wątków
  2. I tutaj w logach zobaczymy, iż pomimo tego, że chcemy wykonania się 100 wątków to tylko trzy jadą w jednej chwili.
run task 93
run task 94
run task 95
run task 96
run task 97
run task 98
run task 99
run task 100
 running thread pool-1-thread-1
 running thread pool-1-thread-3
 running thread pool-1-thread-2
 running thread pool-1-thread-1
 running thread pool-1-thread-3
 running thread pool-1-thread-2

Callable

Tutaj zrobimy jeszcze raz to ćwiczenie, w którym watki liczą sumę elementów kolekcji. Drogi czytelniku zauważ, że tym razem nie ma potrzeby przechowywania żadnego stanu w wątkach ani nic takiego gdyż używamy Callable , które zwyczajnie zwraca rezultat.

object CalablesExample {

  def main(args: Array[String]) {
    val n=100
    val v=Vector.tabulate(n)(i=>i*2)
    val e=Executors.newFixedThreadPool(5)
    
    val callables=(0 to n/10).map(i=>new Summarizer(i*10,v))
    val futures=callables.map(e.submit(_))
    val result=futures.map(_.get).sum
    
    println(result)
    e.shutdown() //1
    e.awaitTermination(10, TimeUnit.SECONDS)
  }
  
  class Summarizer(startIndex:Int, v:Vector[Int]) extends Callable[Int] {
    
    def call(): Int = {
      val dataForThread=v.slice(startIndex, startIndex+10)
      println(s"summarizing elements ${startIndex} : ${startIndex+10} - ${Thread.currentThread().getName}") //6
      dataForThread.sum
    }
    
  }
}
  1. Pamiętajcie wyłączać po sobie światło.

Fork Join

I na koniec bonus z Javy7 - ForkJoin Framework - charakteryzuje się tym, że wątki sobie zabierają pracę jak nic nei robią.

object ForkJoinExample {

  def main(args: Array[String]) {
    val n = 100
    val v = Vector.tabulate(n)(i => i * 2) //1

    val t = new Task(v, 0, v.length)
    val p = new ForkJoinPool() //3
    p.execute(t) 

    p.shutdown()
    p.awaitTermination(10, TimeUnit.SECONDS)
    println(s"result ${t.get}")
  }

  import ForkJoinTask._ //4

  class Task(is: Seq[Int], start: Int, end: Int) extends RecursiveTask[Int] { //5
    def compute(): Int = {
      println(s"calculating ${start} - ${end}")
      if (end - start < 10)
        is.slice(start, end + 1).sum
      else {
        val mid = (start + end) / 2
        val t1 = new Task(is, start, mid)
        val t2 = new Task(is, mid + 1, end)
        invokeAll(t1, t2) //6
        t1.get() + t2.get() //7
      }
    }
  }
}
  1. Tutaj jakieś dane w kolekcji podobnie jak w poprzednich przykładach
  2. Tu był krzyż a teraz już go nie ma
  3. Nowy typ puli - FooooorkJoin
  4. To jest ciekawe , będziemy wykorzystywać statyczne metody z ForkJoinTask i w scali trzeba je niezależnie zaincludować (przynajmniej w ScalaIDE by działało.)
  5. Nowy typ klas do rozdziedziczenia - RecursiveTask[
  6. Statyczna klasa, która wywołuje pod-taski. Jak widać to my musimy zadbać o to by stworzyć ich odpowiednią ilość (i jakość).
  7. Na koniec suma i koniec

Podsumowanie

Jak już wspominałem w jakieś wielkie i wyczerpujące opisy się nie bawiłem bo wszystko omówimy sobie w trakcie warsztatu. Jeśli zajęcia się spodobają to na pewno będziemy je powtarzać i materiał uzupełniać.

To by było tyle jeśli chodzi o cześć pierwszą a w kolejnych : ThreadLocal,Semafory,Phasery,Lacze, Bariery, ThreadFactory i inne takie. A później(Albo w międzyczasie) jakieś najnowsze rozwiązania jak Akka i coś z JAvy8.

Oczywiście sam biorę udział w procesie edukacji, także jeśli ktoś znajdzie błędy w przykładach niech śmiało daje znać.

wtorek, 6 stycznia 2015

Szczęśliwej nauki w nowym roku

Nowy rok Bla bla bla postanowienia sratatata silna wola hejkumkeikum porady .

(Nie)racjonalni agenci

Chcę nauczyć się noweje technologii | schudnąć | dopakować | zmienić coś w swoim życiu.

Chcesz czy "musisz"? Różnica jest istotna i wręcz krytyczna do zrozumienia jak coś w sobie zmienić.

Zazwyczaj ("zazwyczaj") ludzie chcą/zmuszają się gdy:

  • (1) chcą grać w gry komputerowe
  • (2) starają się nauczyć nowego języka bo to może zaprocentować czymś tam w przyszłość. Mają postanowienie, że się nauczą ale jednocześnie mają ochotę porobić coś innego.
  • (1) chcą pić browary - trudno mi sobie wyobrazić sytuację, gdzie ktoś ma postanowienie noworoczne - "będę pić przynajmniej 2 browary dziennie chociaż nie mam ochoty"
  • (2) starają się ćwiczyć regularnie - chociaż woleliby posiedzieć i pooglądać filmy
Wytłumaczyć dlaczego te czynności z "1" wychodzą dla większości łatwiej niż te z "2" można na wysokim poziomie humanistyczno-psychologicznym - ot wystarczy zerknąć na taki model Dorosły,Rodzic i Dziecko. Kiedyś używałem tego do zmotywowanie siebie do treningów krav magi po tym jak dostałem kilka razy z rzędu bęcki na mieście - ale ten model jest tak ogólny jak ...(tutaj można wstawić żart z dowolną ogólną metodyką, która mówi "rób tak i nie pytaj dlaczego")

Zanim pójdziemy dalej zastanówmy się czym się różni granie w gry(na PC) od zwykłego programowania?

  1. Granie w gry - siadasz, masz przed sobą maszynę z pewnym ustawieniem bitów a pamięci. Klikasz myszkę, naciskasz klawiaturę i zapisujesz nowe ustawienie bitów w pamięci
  2. Programowanie- siadasz, masz przed sobą maszynę z pewnym ustawieniem bitów a pamięci. Klikasz myszkę, naciskasz klawiaturę i zapisujesz nowe ustawienie bitów w pamięci
Gdzie więc tkwi różnica?

Hamowanie

Co może nie być oczywiste na pierwszy rzut oka - obydwa powyższe obrazki przedstawiają mózg :). Z tyłu mózgu (w uproszczeniu) znajdują się pierwotne zaś z przodu nowożytne moduły - a jeśli to złe nazwy to w duchu personifikacji nazwijmy je "Staruch" i "Nowak".

Staruch towarzyszy nam od zarania ewolucji i to on ma misję "przetrwaj i rozmnażaj(się)". To on chce natychmiastowej gratyfikacji w postaci węglowodanów prostych, "low hanging frutów" czy tudzież innych łatwych natychmiastowych zysków. To on także każe nam siedzieć i nie wychylać się bo wtedy czujemy się komfortowo. Co bardzo ważny co okaże się póżniej krytyczne Nie ma poczucia czasu jako takiego. Staruch potrafi działać w ramach grupy doskonale rozumiejąc, że jeśli może łatwo i szybko pokonać smoka to zostanie się bohaterem w swoim domu

Nowak pojawił się niedawno i myśli abstrakcyjnie. Rozumie, że on za miesiąc czasu to także on. Że wybory z dnia dzisiejszego mają swoje konsekwencje w przyszłości i że czasami coś co jest nieintuicyjne tu i teraz ma swój sens "kiedyś". A i jeszcze jedno - Nowak wcale nie kontroluje starucha choć chce tak myśleć. To staruch ma więcej doświadczenia w zarządzaniu rdzeniem kręgowym, hormonami i podobnymi mechanizmami. Zawsze gdy Staruch wygrywa spór Nowak może albo czuć się źle albo wkręcić sobie, że on tak naprawdę to lubi dużo jeść/pić/palić a ten odpoczynek mu się zwyczajnie należał a ten koleś, któremu zajął miejsce parkingowe na pewno albo jest zły i mu się należało albo pewnie cwaniak jakiś i mu się no właśnie - należało.

Takie rzeczy niestety się dzieją gdy rozwijamy system bez refaktoringu :(

A jeśli komuś się nie podoba ta metafora to polecam Thninking fast and slow

Notka -Trochę mechaniki

Na mój obecny stan wiedzy w mózgu działa masa niezależnych procesów, które wykorzystują te same materiały do pracy (i znów na obecny stan wiedzy to będzie głównie wapń i coś tam jeszcze). Dlatego też istnieje skończony czas w jaki Nowak może się przeciwstawiać Staruchowi a proces nazywa się Hamowaniem.Np. Staruch chce sobie splunąć a Nowak go powstrzymuje bo przy Policji to tak ryzykownie. Czas ten skraca się po nieprzespanej nocy na skutek zalegania odpadów bio-chemicznych w mózgu (czy coś w ten deseń)

Relacje

Sytuacja 1 - koncepcja hedonistyczna

Staruch chce cukrów bo cukry były dobrym pożywieniem w czasach flinstonów, Nowak chce cukru bo to przyjemne. Staruch chce grać bo w swoim prostym rozumieniu świata zyskuje bogactwa/status/nowy dywan w simsach a z organizmu nie dochodzą żadne sygnały jako by to było męczące. Nowak chce grać bo to przyjemne. Czy ta sytuacja jest dobra czy zła? Generalnie wielu ludzi tak idzie przez życie niektórzy są szczęśliwi inni nie - Szczęście=Rzeczywistość-Oczekiwania.

Ta sytuacja może również wiązać się z czymś raczej pozytywnym. Np. możemy się uzależnić od dobrego samopoczucia po skończonym treningu a po miesiącu odpoczynku od cukrów i przetworzonej żywności nawet jedzenie brukselki może kojarzyć się z przyjemnością

Sytuacja 2 - Konflikt

Staruch chce grać a Nowak chce obejrzeć prezentację o Springu. Staruch chce obejrzeć film a Nowak chce iść na siłkę bo sobie zrobił postanowienie noworoczne. To jest dosyć nieprzyjemny stan z którego można się uwolnić techniką "fake it till you make it" - czyli jakoś zmusić Starucha do robienia czegoś aż mu się to spodoba lub wkręcić starucha, że robienie tego czegoś jest przyjemne- zaraz więcej o tym.

W tym przypadku ma miejsce tzw. "słomiany zapał". Czyli sytuacja gdy Nowak wkręcił Starucha wizualizacjami o tym jak po dwóch tygodniach rozpiętek będzie miał klatę ala Arnold - atu kicha i do tego trzeba jeszcze jakąś dietę utrzymywać - Staruch mówi - "Siadaj i oglądaj film w nie będziesz mi tutaj farmazony w głowie generował"

Sytuacja 3 - Inspiracja podstawowa

Przejdźmy na poziom abstrakcji wyżej - co tak naprawdę "motywuje starucha"? Przez ostatnie kilka milinoów lat dojrzewał on w grupach do 150 osobników gdzie wzajemne relacje i hierarchie decydowały o tym czy żyjesz/nie żyjesz. Jeśli zrobiłeś salto i jednym uderzeniem zabiłeś mamuta zostajesz bohaterem i grupa instynktownie "coś jest ci winna". Jeśli zrobiłeś salto i zabiłeś mamuta ale nikt tego nie widzi to pewnie nikt ci nie uwierzy.

Żarcia było mało, ludzie jakoś funkcjonowali nie jedząc 7 małych dobrze zbilansowanych posiłków dziennie. Nie wiem czy mamuty miały gluten czy nie ale raczej nikt o diecie nie myślał.

Czy nauka nowego języka może jakoś zająć miejsce widowiskowego poskromienia mamuta?

  • Przede wszystkim zabicie mamuta dostarczyło mięsa dla gromady. Niech mięso stanowi pewno wartość. Ucząc się języka możesz potencjalnie zwiększyć swoje szanse na zatrudnienie ale tutaj znowu może pojawić się konflikt bo Staruch woli grać w gry. Sytuacja będzie inna gdy już jesteśmy w miejscu gdzie jest dosyć prosta relacja pomiędzy "nauka języka"~"Profit" - pamiętajmy, ze Staruch ma proste pojmowanie świata. Można również uczyć się języka by nauczyć innych tego języka by oni więcej zarabiali - bardziej abstrakcyjne i trudniejsze do realizacji.
  • No i też w kontekście tego punktu trudniej zmotywować siebie do nauki języka dla samego siebie aniżeli do nauki języka gdy można się tym przed kimś pochwalić - i to w sposób pozytywny pomagając np. na forum.

Sytuacja 4 - Inspiracja wyższych celów

Ponieważ ludzki umysł szuka wszędzie przyczyny i skutku toteż wiele dziwnych rzeczy robiono w imię różnych abstrakcyjnych idei. Pamiętam z lekcji języka polskiego, że jakiś koleś przesiedział 14 lat na drzewie w imię lepszego życia po życiu. Generalnie słowo klucz to "wyższa Idea" czy w imię "wyższej idei spotka mnie coś dobrego". Nie znam dokładnej mechaniki ale jeśli komuś uda się to rozgryźć to zapewne i tędy wiedzie droga do spełnienia postanowień noworocznych.

Sytuacja 1b - antyhedonizm

Tak jak Staruch i Nowak mogą robić przyjemne odczucia tak mogą wspólnie nie lubić nieprzyjemnych. Np. można pobić swój rekord życiowy na setkę gdy gonią nas łysi na mieście. Ewentualnie nie lubimy wychodzić na niekonsekwentnych także jedna słynna technika mówi podziel się planami ze znajomymi - ten miecz ma dwa końce gdyż z drugiej strony "mienie w dupie" tego co myślą o nas inni jest ważnym składnikiem samorozwoju

Sytuacja 6 - Inspiracja braku celów - Nihilizm Egzystencjalny

To jest dosyć ciekawy przypadek - być może na pewnym poziomie rozwoju gdy zagłuszeniu ulegnie naturalna ignorancja a rzeczy oczywiste przestaną napędzać codzienność, wtedy Staruch sam sobie zada pytanie "przetrwać i rozmnażać - ale po co?". Nowak spyta "co robimy?" , "- no chyba szukamy?".

Może to się zakończyć głęboką depresją ale również niezwykłą motywacją w kierunku szeroko pojętych poszukiwań i nowych doświadczeń. Temat trudny ale niektóre postacie przewijające się przez ten nurt maja fajne cytaty :

  • "Ludzie ro­zumieją mnie tak słabo, że nie ro­zumieją na­wet moich narze­kań na to, że mnie nie rozumieją." - Soren Kierkegaard
  • "To niezwykłe, jak można kłamać, przyznając sobie rację." - Jean-Paul Sartre
  • "Życie można zro­zumieć, pat­rząc nań tyl­ko wstecz. Żyć jed­nak trze­ba naprzód." - Soren Kierkegaard
  • "Wszyscy jesteśmy tutaj, aby jeść i pić dla zachowania naszego drogocennego istnienia, a nie ma nic, nic, żadnej racji istnienia." - Jean-Paul Sartre
  • "Człowiek zaw­sze czu­je się czymś więcej niż tym, co osiągnął, więc co­kol­wiek by osiągnął, w niczym nic może zna­leźć uko­jenia, za­dowo­lenia, szczęścia." - Soren Kierkegaard
I tak ogólnie : Nihilizm egzystencjalny

Za długie motyla noga - TL;DR - szbka technika - obcy z przyszłości

  1. Artykuł - Neurologia prokrastynacji
  2. W skrócie - Skany mózgu pokazują, że nasze szare komórki nie traktują "nas tu i teraz" i "nas w przyszłości" jako tę samą osobę. "Ja z przyszłości" i "trzecia osoba z tu i teraz" generują te same reakcje.
  3. Należy stworzyć połączenie poprzez wizualizacje, medytacje i tego typu techniki dzięki czemu Staruch zrozumie, że krzywdząc siebie z przyszłości krzywdzimy siebie
I jeszcze jeden trik na deadlajny : http://www.nytimes.com/2015/01/04/business/if-you-want-to-meet-that-deadline-play-a-trick-on-your-mind.html