Skip to content
Santekno.com | Level Up Your Engineering Skills
ID
📖 0%
01 Jan 2024 · 13 mnt baca ·Artikel 114 / 119
Go

Membuat Generator Image Thumbnail Menggunakan Pipeline Pattern

IH
Ihsan Arif
Penulis di Santekno · Backend Engineer

Generate Image menggunakan Golang juga kita gunakan untuk mempermudah editor agar tidak perlu melakukan edit memakai aplikasi lain sehingga kita bisa mudah menaruh image yang diinginkan. Nah saat ini santekno akan coba membuat generator Image Thumbnail yang sudah ada pada tutorial ini. Misalkan kita ingin membuat thumbnail ini image-nya menjadi lebih ringkas daripada image yang aslinya. Maka kita perlu melakukan convert menjadi file yang lebih ringan dengan ukuran yang kecil. Bagaimana jika image tersebut banyak maka jika kita pakai seperti biasa Golang sequencial maka akan menjadi lama saat kita melakukan eksekusinya. Maka, kita akan coba bandingkan bagaimana proses generate image Thumbnail ini dengan golang yang sequencial dengan menggunakan concurrent Pipeline Patter.

Jika Anda belum pernah mempelajari apa itu Pipeline Pattern bisa dilihat terlebih dahulu tutorial ini.

Persiapan Projek

Sekarang kita akan buat projek baru dengan membuat folder learn-golang-generator-image-thumbnail. Setelah itu buat inisialisasi projek module dengan perintah ini.

bash
1go mod init github.com/santekno/learn-golang-generator-image-thumbnail

Siapkan image atau foto yang dibutuhkan atau bisa ambil photo di repository santekno disini

bash
1https://github.com/santekno/learn-golang-generator-image-thumbnail/tree/main/images

Membuat Generate Image Thumbnail Menggunakan Sequential

Sebelum ke dalam kode kita perlu pahami terlebih dahulu proses point besar yang akan di proses pada generate Image Thumbnail ini. Berikut tahapan proses yang harus kita pahami dan nantinya akan kita bagi menjadi beberapa fungsi sebagai berikut.

  1. Fungsi membaca file image dari folder images/ dengan melakukan validasi file harus berekstensi image.
  2. Fungsi memanipulasi file image dengan mengggunakan library package github.com/disintegration/imaging dengan ukuran 100 x 100 pixel.
  3. Fungsi menyimpan hasil image thumbnail tersebut ke dalam folder thumbnail/

Sudah terbayangkah prosesnya akan seperti apa? Berharap teman-teman bisa memahami proses yang nantinya akan kita buat pada Golang ini.

Lebih jelasnya kita gambarkan ilustrasi prosesnya dibawah ini.

MERMAID
 flowchart LR
    subgraph subGraph1 ["func walkFiles()"]
    C("func\n getFileContentType()") --> D("func\n processImage()")
    D --> E("func\n saveThumbnail()")
    end
    id1((start)) --> d("main func") --> subGraph1 --> e("print \nprocess time") --> id2((finish))

Buatlah file main.go yang mana nantinya semua fungsi-fungsi akan kita buat di file ini. Pertama kita membuat proses generator ini dengan proses sequential biasa.

Fungsi Mengambil Image dari Folder

Fungsi yang akan kita buat ini akan membaca folder yang terdapat beberapa file image sekaligus melakukan pengecekan apakah ini ekstensinya benar image atau tidak. Kita lihat dibawah ini fungsinya.

go
 1func walkFiles(root string) error {
 2	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
 3
 4		// filter out error
 5		if err != nil {
 6			return err
 7		}
 8
 9		// check if it is file
10		if !info.Mode().IsRegular() {
11			return nil
12		}
13
14		// check if it is image/jpeg
15		contentType, _ := getFileContentType(path)
16		if contentType != "image/jpeg" {
17			return nil
18		}
19
20		return nil
21	})
22
23	if err != nil {
24		return err
25	}
26	return nil
27}
28
29// getFileContentType - return content type and error status
30func getFileContentType(file string) (string, error) {
31
32	out, err := os.Open(file)
33	if err != nil {
34		return "", err
35	}
36	defer out.Close()
37
38	// Only the first 512 bytes are used to sniff the content type.
39	buffer := make([]byte, 512)
40
41	_, err = out.Read(buffer)
42	if err != nil {
43		return "", err
44	}
45
46	// Use the net/http package's handy DectectContentType function. Always returns a valid
47	// content-type by returning "application/octet-stream" if no others seemed to match.
48	contentType := http.DetectContentType(buffer)
49
50	return contentType, nil
51}
Pada kode diatas bisa kita lihat ada 2 fungsi yang sudah kita buat yaitu fungsi walkFiles yang berguna untuk membaca file yang ada dalam satu folder yang dikirim dari parameter, lalu fungsi yang kedua yaitu getFileContentType berguna untuk mengecek fil tersebut memiliki tipe content dalam artian tipe ektensinya apakah image atau bukan sehingga ketika kita ingin membuat thumbnail nanti saat di generate tidak semua file yang support hanya image saja sehingga sudah tersaring dari awal hanya image yang bisa di generate oleh program kita.

Fungsi Memanipulasi File Image

Fugsi ini adalah proses untuk melakukan perubahan image yang akan di compress menjadi tipe thumbnail image yang mana ukurannya akan menjadi 100x100 pixel. Dalam fungsi ini kita ada bantuan menggunakan library tambahan yaitu library github.com/disintegration/imaging. Maka kita perlu tambahkan terlebih dahulu library tersebut dengan eksekusi ini

bash
1go get -u github.com/disintegration/imaging

Selanjutnya tambahkan file main.go dengan fungsi dibawahnya seperti ini.

go
 1// processImage - takes image file as input
 2// return pointer to thumbnail image in memory.
 3func processImage(path string) (*image.NRGBA, error) {
 4
 5	// load the image from file
 6	srcImage, err := imaging.Open(path)
 7	if err != nil {
 8		return nil, err
 9	}
10
11	// scale the image to 100px * 100px
12	thumbnailImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
13
14	return thumbnailImage, nil
15}

Dan jangan lupa update dan tambahkan pada fungsi walkFiles() untuk mengakses fungsi processImage ini setelah dilakukan pengecekan image.

go
 1func walkFiles(root string) error {
 2	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
 3
 4		...
 5
 6		// process the image
 7		thumbnailImage, err := processImage(path)
 8		if err != nil {
 9			return err
10		}
11
12    ...
13
14		return nil
15	})
16
17	if err != nil {
18		return err
19	}
20	return nil
21}

Fungsi Menyimpan Hasil Image Thumbnail

Proses yang ketika kita akan menyimpan hasil image thumbnail ini ke dalam satu folder dengan nama folder thumbnail/. Nantinya hasil dari generate image fungsi processImage berupa file thumbnailImage maka akan kita simpan file hasil dari generator image fungsi tersebut ke dalam satu folder. Berikut lebih lengkapnya seperti dibawah ini.

go
 1// saveThumbnail - save the thumnail image to folder
 2func saveThumbnail(srcImagePath string, thumbnailImage *image.NRGBA) error {
 3	filename := filepath.Base(srcImagePath)
 4	dstImagePath := "thumbnails/" + filename
 5
 6	// save the image in the thumbnail folder.
 7	err := imaging.Save(thumbnailImage, dstImagePath)
 8	if err != nil {
 9		return err
10	}
11	fmt.Printf("%s -> %s\n", srcImagePath, dstImagePath)
12	return nil
13}

Berarti siapkan juga folder hasil save image thumbnail-nya pada folder ini thumbnails/. dan fungsi tersebut juga akan kita akses pada fungsi walFiles() setelah memanggil fungsi processImage().

go
 1func walkFiles(root string) error {
 2	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
 3
 4    ..
 5
 6		// process the image
 7		thumbnailImage, err := processImage(path)
 8		if err != nil {
 9			return err
10		}
11
12		// save the thumbnail image to disk
13		err = saveThumbnail(path, thumbnailImage)
14		if err != nil {
15			return err
16		}
17		return nil
18	})
19
20  ...
21
22	return nil
23}
Proses generator Image thumbnail sudah siap dengan proses standar sequential, kita coba jalankan dengan perintah dibawah ini.
bash
 1➜  learn-golang-generator-image-thumbnail git:(main) ✗ ./learn-golang-generator-image-thumbnail images
 2images/sample-1.jpg -> thumbnails/sample-1.jpg
 3images/sample-10.jpg -> thumbnails/sample-10.jpg
 4images/sample-11.jpg -> thumbnails/sample-11.jpg
 5images/sample-12.jpg -> thumbnails/sample-12.jpg
 6images/sample-13.jpg -> thumbnails/sample-13.jpg
 7images/sample-14.jpg -> thumbnails/sample-14.jpg
 8images/sample-2.jpg -> thumbnails/sample-2.jpg
 9images/sample-3.jpg -> thumbnails/sample-3.jpg
10images/sample-4.jpg -> thumbnails/sample-4.jpg
11images/sample-5.jpg -> thumbnails/sample-5.jpg
12images/sample-6.jpg -> thumbnails/sample-6.jpg
13images/sample-7.jpg -> thumbnails/sample-7.jpg
14images/sample-8.jpg -> thumbnails/sample-8.jpg
15images/sample-9.jpg -> thumbnails/sample-9.jpg
16Time taken: 145.78275ms
Hasil dari proses membuat generator Image thumbnail ini sekitar 145ms cukup cepat karena memang image yang kita pakai tidak terlalu banyak hanya 14 file image saja.

Mengubah Mekanisme Proses menggunakan Pipeline Pattern Concurrent Golang

Kita sudah lihat diatas ketika menggunakan sequential proses untuk generate image thumbnail sebanyak 14 image membutuhkan sekitar 145ms. Jika kita hitung satuanya yaitu dibagi 14 menjadi 14ms setiap satu image yang diproses. Maka jika kita memiliki 1 juta image waktu yg dibutuhkan sekitar 14ms x 1jt = 14.000.000ms atau 3.89 hours. Ini cukup lama jika ingin proses data sebesar itu. Maka kita akan coba implementasikan Pipeline Pattern ini apakah bisa mengurangi proses menjadi lebih cepat atau tidak.

Pertama kita perlu ada beberapa perubahan kode. Agar kode program yang sebelumnya kita tidak dihapus maka kita buat folder sequential untuk memindahkan kode yang sebelumnya kita buat ke dalam folder tersebut. Lalu kita buat folder baru lagi yaitu pipeline-pattern sehingga struktur folder pada projek akan seperti ini.

bash
 1.
 2├── README.md
 3├── learn-golang-generator-image-thumbnail
 4├── go.mod
 5├── go.sum
 6├── images
 7│   ├── sample-1.jpg
 8│   ├── sample-10.jpg
 9│   ├── sample-11.jpg
10│   ├── sample-12.jpg
11│   ├── sample-13.jpg
12│   ├── sample-14.jpg
13│   ├── sample-2.jpg
14│   ├── sample-3.jpg
15│   ├── sample-4.jpg
16│   ├── sample-5.jpg
17│   ├── sample-6.jpg
18│   ├── sample-7.jpg
19│   ├── sample-8.jpg
20│   └── sample-9.jpg
21├── main.go
22├── pipeline-pattern
23│   └── pipeline.go
24├── sequential
25│   └── sequential.go
26└── thumbnails

Sesuai dengan struktur folder yang sudah kita buat maka fungsi-fungsi yang berhubungan dengan sequential ada pada folder sequential sedangkan untuk yang akan kita buat sekarang yaitu pipeline pattern pada folder pipeline-pattern. Kita coba langsung buat pada file pipeline.go.

Pertama kita perlu struct untuk membantu pengiriman data pipeline yang standar sehingga tiap proses akan menerima data struct yang sama seperti ini.

go
1type result struct {
2	srcImagePath   string
3	thumbnailImage *image.NRGBA
4	err            error
5}

Mengubah Fungsi Mengambil Image dari Folder

Pada file pipeline.go kita buat fungsi yang sama yaitu walkFiles() tetapi ada beberapa yang harus kita ubah diantaranya parameter diubah menjadi tipe channel yang nantinya bisa asynchronous saat program dijalankan.

go
 1func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) {
 2	// create output channels
 3	paths := make(chan string)
 4	errc := make(chan error, 1)
 5
 6	go func() {
 7		defer close(paths)
 8		errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
 9			// filter out error
10			if err != nil {
11				return err
12			}
13
14			// check if it is file
15			if !info.Mode().IsRegular() {
16				return nil
17			}
18
19			// check if it is image/jpeg
20			contentType, _ := sequential.GetFileContentType(path)
21			if contentType != "image/jpeg" {
22				return nil
23			}
24
25			// send file path to next stage
26			select {
27			case paths <- path:
28			case <-done:
29				return fmt.Errorf("walk cancelled")
30			}
31			return nil
32		})
33	}()
34	return paths, errc
35}

Proses diatas akan berjalan menggunakan goroutine yang akan mengirimkan file-file dibaca dan dikirim path file-nya agar bisa diproses ke fungsi selanjutnya. Lalu pemanggilan fungsi sequential.GetFileContentType sebagai validasi ini kita ambil dari package sebelumnya pada folder sequential. Maka perlu ada update fungsi tersebut menjadi fungsi global dengan mengubah agar bisa diakses diberbagai package dari

go
1func getFileContentType(file string) (string, error)
menjadi
go
1func GetFileContentType(file string) (string, error)

Mengubah Fungsi Memanipulasi File Image

Pada fungsi manipulasi secara proses sama tetapi kita akan menerapkan channeling yang mana fungsi tersebut bisa diproses secara paralel. Berikut lebih lengkapnya dibawah ini.

go
 1func processImage(done <-chan struct{}, paths <-chan string) <-chan result {
 2	results := make(chan result)
 3	var wg sync.WaitGroup
 4
 5	thumbnailer := func() {
 6		for srcImagePath := range paths {
 7			srcImage, err := imaging.Open(srcImagePath)
 8			if err != nil {
 9				select {
10				case results <- result{srcImagePath, nil, err}:
11				case <-done:
12					return
13				}
14			}
15			thumbnailImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)
16
17			select {
18			case results <- result{srcImagePath, thumbnailImage, err}:
19			case <-done:
20				return
21			}
22		}
23	}
24
25	const numThumbnailer = 5
26	for i := 0; i < numThumbnailer; i++ {
27		wg.Add(1)
28		go func() {
29			thumbnailer()
30			wg.Done()
31		}()
32	}
33
34	go func() {
35		wg.Wait()
36		close(results)
37	}()
38
39	return results
40}
Bisa kita lihat proses processImage() ini menjadi lebih rumit karena kita sedang mengimplementasikan channel dan goroutine sehingga proses tidak perlu saling menunggu dikarenakan proses yang kita lakukan itu berdasarkan proses yang dikirim oleh channel paths. Selama channel paths ini masih ada data yang dikirim maka fungsi ini akan terus bekerja.

Mengubah Menjadi Global pada Fungsi Menyimpan Hasil Image Thumbnail

Pada fungsi save thumbnail juga kita akan ubah parameter menjadi channel seperti dibawah ini.

go
 1func saveThumbnail(done <-chan struct{}, thumbs <-chan result) <-chan result {
 2	results := make(chan result)
 3	var wg sync.WaitGroup
 4
 5	saveThumbnailer := func() {
 6		for img := range thumbs {
 7			filename := filepath.Base(img.srcImagePath)
 8			dstImagePath := "thumbnails/" + filename
 9
10			// save the image in the thumbnail folder.
11			err := imaging.Save(img.thumbnailImage, dstImagePath)
12			if err != nil {
13				select {
14				case results <- result{img.srcImagePath, dstImagePath, img.thumbnailImage, err}:
15				case <-done:
16					return
17				}
18			}
19			select {
20			case results <- result{img.srcImagePath, dstImagePath, img.thumbnailImage, err}:
21			case <-done:
22				return
23			}
24		}
25	}
26
27	const numGoroutine = 5
28	for i := 0; i < numGoroutine; i++ {
29		wg.Add(1)
30		go func() {
31			saveThumbnailer()
32			wg.Done()
33		}()
34	}
35
36	go func() {
37		wg.Wait()
38		close(results)
39	}()
40
41	return results
42}

Membuat Fungsi SetupPipeline

Fungsi SetupPipeline ini digunakan untuk mengumpulkan semua proses goroutine yang sedang berjalan menjadi satu fungsi yang nantinya bisa diakses oleh fungsi main lebih mudah.

go
 1func SetupPipeLine(root string) error {
 2	done := make(chan struct{})
 3	defer close(done)
 4
 5	// do the file walk
 6	paths, errc := walkFiles(done, root)
 7
 8	// process the image
 9	resultImages := processImage(done, paths)
10
11	// save thumbnail images
12	results := saveThumbnail(done, resultImages)
13
14	// save thumbnail images
15	for r := range results {
16		if r.err != nil {
17			return r.err
18		}
19		fmt.Printf("%s -> %s\n", r.srcImagePath, r.destImagePath)
20	}
21
22	// check for error on the channel, from walkfiles stage.
23	if err := <-errc; err != nil {
24		return err
25	}
26
27	return nil
28}

Semua fungsi sudah kita buat untuk kebutuhan pipeline pattern generate image ini, maka tinggal kita coba jalankan program dengan mengubah terlebih dahulu file main.go karena sebelumnya kita sudah memakai fungsi yang sequential sekarang kita pakai pipeline pattern yang sudah kita buat.

go
 1// Image processing - sequential
 2// Input - directory with images.
 3// output - thumbnail images
 4func main() {
 5	if len(os.Args) < 2 {
 6		log.Fatal("need to send directory path of images")
 7	}
 8	start := time.Now()
 9
10	// using sequential
11	// err := sequensial.WalkFiles(os.Args[1])
12
13	// using pipeline pattern
14	err := pipelinepattern.SetupPipeLine(os.Args[1])
15
16	if err != nil {
17		log.Fatal(err)
18	}
19	fmt.Printf("Time taken: %s\n", time.Since(start))
20}

Terlihat diatas penggunaan fungsi sequential kita komentar terlebih dahulu agar tidak di eksekusi pada saat program jalan. Jalankan program dengan perintah yang sama seperti yang diatas yaitu

bash
1go run main.go images

Hasil dari proses yang akan terlihat 2 kali lebih cepat yaitu kurang lebih 64ms

bash
 1➜  learn-golang-generator-image-thumbnail git:(main) ✗ go run main.go images
 2images/sample-11.jpg -> thumbnails/sample-11.jpg
 3images/sample-10.jpg -> thumbnails/sample-10.jpg
 4images/sample-1.jpg -> thumbnails/sample-1.jpg
 5images/sample-12.jpg -> thumbnails/sample-12.jpg
 6images/sample-13.jpg -> thumbnails/sample-13.jpg
 7images/sample-14.jpg -> thumbnails/sample-14.jpg
 8images/sample-4.jpg -> thumbnails/sample-4.jpg
 9images/sample-2.jpg -> thumbnails/sample-2.jpg
10images/sample-3.jpg -> thumbnails/sample-3.jpg
11images/sample-5.jpg -> thumbnails/sample-5.jpg
12images/sample-6.jpg -> thumbnails/sample-6.jpg
13images/sample-7.jpg -> thumbnails/sample-7.jpg
14images/sample-8.jpg -> thumbnails/sample-8.jpg
15images/sample-9.jpg -> thumbnails/sample-9.jpg
16Time taken: 64.981125ms

Hasil Percobaan

Berikut ini tabel hasil percobaan dengan jumlah data image yang lebih besar sehingga terlihat perbedaan waktu proses antara kedua flow yang sudah kita gunakan.

Jumlah DataSequentialPipeline Pattern
14145.78ms64.98ms
179217.38s6.46s
358433.51s12.09s
14.3362m18.07s50.83s

Kesimpulan

Pipeline Pattern ini sangat berguna sekali ketika kita memiliki proses yang saling keterkaitan tetapi datanya banyak dan setiap data yang satu ke data yang lain tidak perlu menunggu sehingga prosesnya bisa kita paralelkan. Bermanfaat sekali ketika proses seperti ini kita implementasikan agar lebih mengefisiensikan proses sehingga setiap data yang akan di proses secara sequensial tersebut tidak perlu menunggu proses data sebelumnya selesai.

Hal ini terlihat dari percobaan yang kita lakukan dengan cara membandingkan kedua proses yang pertama yaitu menggunakan proses sequential dimana setiap data saling menunggu proses sampai selesai, sedangkan proses yang kedua menggunakan pipeline pattern yang mana data pertama, data kedua dan seterusnya tidak perlu menunggu proses sebelumnya selesai, asalkan setiap data memiliki runtutan proses yang sama sehingga memberikan pengolahan proses data yang lebih cepat bisa sampai 2 kali dari proses menggunakan sequential biasa.

Pada percobaan ini memang tidak memiliki data yang besar hanya 14 image, tetapi jika teman-teman ingin mencoba eksplorasi lebih lanjut bisa tambahkan image-nya lebih banyak agar bisa terlihat apakah prosesnya lebih efisien atau tidak.

Artikel Terkait

💬 Komentar