bzip2

bzip2

  • Correndo atrás do tempo perdido com Go!

    RoadRunner 4 3371596983

    Estou atualmente trabalhando numa empresa onde uma das visões da empresa é conseguir ter os dados do jogo finalizado disponível na interface de visualização em menos de 1 minuto.   Quase todo o código é escrito em typescript e uma pequena parte em rust.

    Uma das partes mais pesadas é feita em shell, que é a parte de baixar e descompactar um arquivo da steam.  Pra ilustrar o que é feito, fiz esse script em shell que também serve como base de comparação de tempo.

    
    #! /usr/bin/env bash
    #
    
    TARGET_URL="http://replay272.valve.net/730/003705744548740202576_0842061407.dem.bz2"
    DESTINATION="003705744548740202576_0842061407.dem.bz2"
    UNPACKED="003705744548740202576_0842061407.dem"
    CURLUNPACKED="curl-003705744548740202576_0842061407.dem"
    
    die() {
      echo "ERROR: $@" &>2
    }
    
    rm -f $DESTINATION $UNPACKED $CURLUNPACKED
    
    curl -o $DESTINATION \
      -L \
      $TARGET_URL ||
      die "Failed to download: $TARGET_URL"
    
    bunzip2 $DESTINATION ||
      die "Failed to unzip $DESTINATION"
    
    mv $UNPACKED $CURLUNPACKED
    

     O código então baixa esse link da steam e tem de descompactar o arquivo.  Qual é a velocidade dele?

    
    helio@goosfraba ~/t/godownloader> time ./curl-downloader-2.sh
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
    100  216M  100  216M    0     0  60.3M      0  0:00:03  0:00:03 --:--:-- 60.4M
    
    ________________________________________________________
    Executed in   29.61 secs    fish           external
       usr time   25.24 secs  585.00 micros   25.24 secs
       sys time    0.89 secs   71.00 micros    0.89 secs
    

     Podemos ver que o "curl" baixa o arquivo em mais ou menos 3 segundos, e gasta no total 25s, ou seja, uns 22s pra descompactar.  O total termina como 29s, mas vamos focar primeiramente no "usr time".

    Então escrevi um código em Go pra tentar fazer isso de forma mais rápida.

    package main
    
    import (
            "compress/bzip2"
            "fmt"
            "io"
            "net/http"
            "os"
            "time"
    )
    
    const (
            TARGET_URL   = "http://replay272.valve.net/730/003705744548740202576_0842061407.dem.bz2"
            DESTINATION  = "003705744548740202576_0842061407.dem.bz2"
            DECOMPRESSED = "go-003705744548740202576_0842061407.dem"
    )
    
    func main() {
            err := download(TARGET_URL, DESTINATION)
            if err != nil {
                    panic(err)
            }
    
            err = bunzip2(DESTINATION, DECOMPRESSED)
            if err != nil {
                    panic(err)
            }
    }
    
    func download(from, to string) error {
            fmt.Println("Downloading:", from)
            timeStart := time.Now()
            resp, err := http.Get(from)
            if err != nil {
                    return err
            }
    
            defer resp.Body.Close()
            out, err := os.Create(to)
            if err != nil {
                    return err
            }
            defer out.Close()
    
            _, err = io.Copy(out, resp.Body)
            if err != nil {
                    return err
            }
            fmt.Println("Finished:", time.Since(timeStart).String())
    
            return nil
    }
    
    func bunzip2(from, to string) error {
            // https://gist.github.com/rickt/7817401
            fmt.Println("Unpacking:", from)
            timeStart := time.Now()
            pr, pw := io.Pipe()
    
            go func() {
                    defer pw.Close()
                    var inFile *os.File
                    var err error
                    inFile, err = os.Open(from)
                    defer inFile.Close()
                    if err != nil {
                            panic(err)
                    }
    
                    _, err = io.Copy(pw, inFile)
                    if err != nil {
                            panic(err)
                    }
            }()
    
            defer pr.Close()
            z := bzip2.NewReader(pr)
            var outFile *os.File
            var err error
            outFile, err = os.Create(to)
            defer outFile.Close()
            if err != nil {
                    return err
            }
    
            _, err = io.Copy(outFile, z)
            if err != nil {
                    return err
            }
            fmt.Println("Finished:", time.Since(timeStart).String())
            return nil
    }
    

    O código baixa o conteúdo num arquivo e depois descompacta.  Qual a velocidade?

    helio@goosfraba ~/t/godownloader> go build -o go-downloader-2 main-2.go; time ./go-downloader-2 
    Downloading: http://replay272.valve.net/730/003705744548740202576_0842061407.dem.bz2
    Finished: 3.536812275s
    Unpacking: 003705744548740202576_0842061407.dem.bz2
    Finished: 30.220663099s
    
    ________________________________________________________
    Executed in   33.76 secs    fish           external
       usr time   29.90 secs    0.00 micros   29.90 secs
       sys time    0.95 secs  639.00 micros    0.95 secs
    

    Muito lento.  Pior do que eu esperava.  Ele baixa o arquivo em 3.5s, mas leva 30s pra descompactar.  Um dos motivos é com certeza porquê eu salvo em arquivo e depois abro o arquivo pra descompactar.  Vamos então pra próxima versão onde eu passo io.Reader de um pro outro.

    package main
    
    import (
            "compress/bzip2"
            "fmt"
            "io"
            "net/http"
            "os"
            "time"
    )
    
    const (
            TARGET_URL   = "http://replay272.valve.net/730/003705744548740202576_0842061407.dem.bz2"
            DESTINATION  = "003705744548740202576_0842061407.dem.bz2"
            DECOMPRESSED = "go-003705744548740202576_0842061407.dem"
    )
    
    func main() {
            data, err := readerDownload(TARGET_URL, DESTINATION)
            if err != nil {
                    panic(err)
            }
    
            err = bunzip2Stream(data, DECOMPRESSED)
            if err != nil {
                    panic(err)
            }
    }
    
    func readerDownload(from, to string) ([]byte, error) {
            fmt.Println("Downloading:", from)
            timeStart := time.Now()
            resp, err := http.Get(from)
            if err != nil {
                    return nil, err
            }
    
            defer resp.Body.Close()
            content, err := io.ReadAll(resp.Body)
            if err != nil {
                    panic(err)
            }
            fmt.Println("Finished:", time.Since(timeStart).String())
            return content, nil
    
    }
    
    func bunzip2Stream(from []byte, to string) error {
            // https://gist.github.com/rickt/7817401
            fmt.Println("Unpacking:", to)
            timeStart := time.Now()
            pr, pw := io.Pipe()
    
            go func() {
                    defer pw.Close()
    
                    //_, err := io.Copy(pw, from)
                    pw.Write(from)
                    //if err != nil {
                    //              panic(err)
                    //      }
            }()
    
            defer pr.Close()
            z := bzip2.NewReader(pr)
            var outFile *os.File
            var err error
            outFile, err = os.Create(to)
            defer outFile.Close()
            if err != nil {
                    return err
            }
    
            _, err = io.Copy(outFile, z)
            if err != nil {
                    return err
            }
            fmt.Println("Finished:", time.Since(timeStart).String())
            return nil
    }
    
    

    Qual o desempenho?

    helio@goosfraba ~/t/godownloader> go build -o go-downloader-3 main-3.go; time ./go-downloader-3
    Downloading: http://replay272.valve.net/730/003705744548740202576_0842061407.dem.bz2
    Finished: 3.624793323s
    Unpacking: go-003705744548740202576_0842061407.dem
    Finished: 29.883794408s
    
    ________________________________________________________
    Executed in   33.52 secs    fish           external
       usr time   30.03 secs  580.00 micros   30.03 secs
       sys time    0.96 secs   69.00 micros    0.96 secs
    

    Melhorou mas ainda estou longe de fazer melhor que a versão em shell script.  O tempo de descompactar baixou irrisóriamente 1s.  Mesmo não tendo SSD ou NVME meu disco é rápido o suficiente pra isso não impactar ao todo.

    Comecei a pesquisar como poderia melhorar o desempanho do pacote bzip2 do Go! e existe uma discussão sobre isso em aberto.

    https://github.com/golang/go/issues/6754

    A reclamação é sobre versões mais antigas de Go! e até Robert Pike opinia.  Acho que melhorou bastante pros dias de hoje, mas continua lento se comparado com o binário do programa.  O que fazer então?  Declarar derrota?

    Talvez.  Mas ao invés disso eu passei a procurar outras libs no GitHub.  E encontrei o uso do pbzip2, que diz ser mais rápido queo bzip2 do standard.  Então vamos ao código:

    package main
    
    import (
            "context"
            "fmt"
            "io"
            "net/http"
            "os"
            "time"
    
            "github.com/cosnicolaou/pbzip2"
    )
    
    const (
            TARGET_URL   = "http://replay272.valve.net/730/003705744548740202576_0842061407.dem.bz2"
            DESTINATION  = "003705744548740202576_0842061407.dem.bz2"
            DECOMPRESSED = "go-003705744548740202576_0842061407.dem"
    )
    
    func main() {
            data, err := readerDownload(TARGET_URL, DESTINATION)
            if err != nil {
                    panic(err)
            }
    
            err = pbunzip2Stream(data, DECOMPRESSED)
            if err != nil {
                    panic(err)
            }
    }
    
    func readerDownload(from, to string) ([]byte, error) {
            fmt.Println("Downloading:", from)
            timeStart := time.Now()
            resp, err := http.Get(from)
            if err != nil {
                    return nil, err
            }
    
            defer resp.Body.Close()
            content, err := io.ReadAll(resp.Body)
            if err != nil {
                    panic(err)
            }
            fmt.Println("Finished:", time.Since(timeStart).String())
            return content, nil
    
    }
    
    func pbunzip2Stream(from []byte, to string) error {
            // https://gist.github.com/rickt/7817401
            fmt.Println("Unpacking:", to)
            timeStart := time.Now()
            pr, pw := io.Pipe()
    
            go func() {
                    defer pw.Close()
    
                    //_, err := io.Copy(pw, from)
                    pw.Write(from)
                    //if err != nil {
                    //              panic(err)
                    //      }
            }()
    
            defer pr.Close()
            ctx := context.Background()
            z := pbzip2.NewReader(ctx, pr)
            var outFile *os.File
            var err error
            outFile, err = os.Create(to)
            defer outFile.Close()
            if err != nil {
                    return err
            }
    
            _, err = io.Copy(outFile, z)
            if err != nil {
                    return err
            }
            fmt.Println("Finished:", time.Since(timeStart).String())
            return nil
    }
    

    E finalmente, a medida de desempenho:

    helio@goosfraba ~/t/godownloader> go build -o go-downloader-4 main-4.go; time ./go-downloader-4
    Downloading: http://replay272.valve.net/730/003705744548740202576_0842061407.dem.bz2
    Finished: 3.74774932s
    Unpacking: go-003705744548740202576_0842061407.dem
    Finished: 6.108665607s
    
    ________________________________________________________
    Executed in    9.89 secs    fish           external
       usr time   44.22 secs    0.00 micros   44.22 secs
       sys time    0.78 secs  655.00 micros    0.78 secs
    

    Vitória!  O pbzip2 que faz a descompressão em blocos em paralelo levou 6s.  Isso sim é performance.  O "usr time" mostra 44s por algum motivo bizarro, mas o tempo total foi por volta de 10s.  E o resultado?

    helio@goosfraba ~/t/godownloader> sha256sum curl-003705744548740202576_0842061407.dem \
    go-003705744548740202576_0842061407.dem eca9bdd943521251b8704397e40b7f9aada539698561a6c1aca58ebf2602bfc1 curl-003705744548740202576_0842061407.dem eca9bdd943521251b8704397e40b7f9aada539698561a6c1aca58ebf2602bfc1 go-003705744548740202576_0842061407.dem

    Então foi baixado e descompactado bem mais rápido e sem corromper os dados.

    Um ponto a ser visto é que talvez exista também um binário pronto com pbzip2 pra descompactar.  E pode ser que seja mais rápido que em Go!  Mas é pra isso que servem os desafios.  Por enquanto vou celebrar minha pequena vitória com uma cerveja.

    victory

We use cookies

We use cookies on our website. Some of them are essential for the operation of the site, while others help us to improve this site and the user experience (tracking cookies). You can decide for yourself whether you want to allow cookies or not. Please note that if you reject them, you may not be able to use all the functionalities of the site.