pemrograman

Membuat Generator Image Thumbnail Menggunakan Pipeline Pattern

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.

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

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

https://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.

 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.

func walkFiles(root string) error {
	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {

		// filter out error
		if err != nil {
			return err
		}

		// check if it is file
		if !info.Mode().IsRegular() {
			return nil
		}

		// check if it is image/jpeg
		contentType, _ := getFileContentType(path)
		if contentType != "image/jpeg" {
			return nil
		}

		return nil
	})

	if err != nil {
		return err
	}
	return nil
}

// getFileContentType - return content type and error status
func getFileContentType(file string) (string, error) {

	out, err := os.Open(file)
	if err != nil {
		return "", err
	}
	defer out.Close()

	// Only the first 512 bytes are used to sniff the content type.
	buffer := make([]byte, 512)

	_, err = out.Read(buffer)
	if err != nil {
		return "", err
	}

	// Use the net/http package's handy DectectContentType function. Always returns a valid
	// content-type by returning "application/octet-stream" if no others seemed to match.
	contentType := http.DetectContentType(buffer)

	return contentType, nil
}

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

go get -u github.com/disintegration/imaging

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

// processImage - takes image file as input
// return pointer to thumbnail image in memory.
func processImage(path string) (*image.NRGBA, error) {

	// load the image from file
	srcImage, err := imaging.Open(path)
	if err != nil {
		return nil, err
	}

	// scale the image to 100px * 100px
	thumbnailImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)

	return thumbnailImage, nil
}

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

func walkFiles(root string) error {
	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {

		...

		// process the image
		thumbnailImage, err := processImage(path)
		if err != nil {
			return err
		}

    ...

		return nil
	})

	if err != nil {
		return err
	}
	return nil
}

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.

// saveThumbnail - save the thumnail image to folder
func saveThumbnail(srcImagePath string, thumbnailImage *image.NRGBA) error {
	filename := filepath.Base(srcImagePath)
	dstImagePath := "thumbnails/" + filename

	// save the image in the thumbnail folder.
	err := imaging.Save(thumbnailImage, dstImagePath)
	if err != nil {
		return err
	}
	fmt.Printf("%s -> %s\n", srcImagePath, dstImagePath)
	return nil
}

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().

func walkFiles(root string) error {
	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {

    ..

		// process the image
		thumbnailImage, err := processImage(path)
		if err != nil {
			return err
		}

		// save the thumbnail image to disk
		err = saveThumbnail(path, thumbnailImage)
		if err != nil {
			return err
		}
		return nil
	})

  ...

	return nil
}

Proses generator Image thumbnail sudah siap dengan proses standar sequential, kita coba jalankan dengan perintah dibawah ini.

➜  learn-golang-generator-image-thumbnail git:(main) ✗ ./learn-golang-generator-image-thumbnail images
images/sample-1.jpg -> thumbnails/sample-1.jpg
images/sample-10.jpg -> thumbnails/sample-10.jpg
images/sample-11.jpg -> thumbnails/sample-11.jpg
images/sample-12.jpg -> thumbnails/sample-12.jpg
images/sample-13.jpg -> thumbnails/sample-13.jpg
images/sample-14.jpg -> thumbnails/sample-14.jpg
images/sample-2.jpg -> thumbnails/sample-2.jpg
images/sample-3.jpg -> thumbnails/sample-3.jpg
images/sample-4.jpg -> thumbnails/sample-4.jpg
images/sample-5.jpg -> thumbnails/sample-5.jpg
images/sample-6.jpg -> thumbnails/sample-6.jpg
images/sample-7.jpg -> thumbnails/sample-7.jpg
images/sample-8.jpg -> thumbnails/sample-8.jpg
images/sample-9.jpg -> thumbnails/sample-9.jpg
Time 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.

.
├── README.md
├── learn-golang-generator-image-thumbnail
├── go.mod
├── go.sum
├── images
│   ├── sample-1.jpg
│   ├── sample-10.jpg
│   ├── sample-11.jpg
│   ├── sample-12.jpg
│   ├── sample-13.jpg
│   ├── sample-14.jpg
│   ├── sample-2.jpg
│   ├── sample-3.jpg
│   ├── sample-4.jpg
│   ├── sample-5.jpg
│   ├── sample-6.jpg
│   ├── sample-7.jpg
│   ├── sample-8.jpg
│   └── sample-9.jpg
├── main.go
├── pipeline-pattern
│   └── pipeline.go
├── sequential
│   └── sequential.go
└── 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.

type result struct {
	srcImagePath   string
	thumbnailImage *image.NRGBA
	err            error
}

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.

func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) {
	// create output channels
	paths := make(chan string)
	errc := make(chan error, 1)

	go func() {
		defer close(paths)
		errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
			// filter out error
			if err != nil {
				return err
			}

			// check if it is file
			if !info.Mode().IsRegular() {
				return nil
			}

			// check if it is image/jpeg
			contentType, _ := sequential.GetFileContentType(path)
			if contentType != "image/jpeg" {
				return nil
			}

			// send file path to next stage
			select {
			case paths <- path:
			case <-done:
				return fmt.Errorf("walk cancelled")
			}
			return nil
		})
	}()
	return paths, errc
}

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

func getFileContentType(file string) (string, error)

menjadi

func 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.

func processImage(done <-chan struct{}, paths <-chan string) <-chan result {
	results := make(chan result)
	var wg sync.WaitGroup

	thumbnailer := func() {
		for srcImagePath := range paths {
			srcImage, err := imaging.Open(srcImagePath)
			if err != nil {
				select {
				case results <- result{srcImagePath, nil, err}:
				case <-done:
					return
				}
			}
			thumbnailImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos)

			select {
			case results <- result{srcImagePath, thumbnailImage, err}:
			case <-done:
				return
			}
		}
	}

	const numThumbnailer = 5
	for i := 0; i < numThumbnailer; i++ {
		wg.Add(1)
		go func() {
			thumbnailer()
			wg.Done()
		}()
	}

	go func() {
		wg.Wait()
		close(results)
	}()

	return results
}

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.

func saveThumbnail(done <-chan struct{}, thumbs <-chan result) <-chan result {
	results := make(chan result)
	var wg sync.WaitGroup

	saveThumbnailer := func() {
		for img := range thumbs {
			filename := filepath.Base(img.srcImagePath)
			dstImagePath := "thumbnails/" + filename

			// save the image in the thumbnail folder.
			err := imaging.Save(img.thumbnailImage, dstImagePath)
			if err != nil {
				select {
				case results <- result{img.srcImagePath, dstImagePath, img.thumbnailImage, err}:
				case <-done:
					return
				}
			}
			select {
			case results <- result{img.srcImagePath, dstImagePath, img.thumbnailImage, err}:
			case <-done:
				return
			}
		}
	}

	const numGoroutine = 5
	for i := 0; i < numGoroutine; i++ {
		wg.Add(1)
		go func() {
			saveThumbnailer()
			wg.Done()
		}()
	}

	go func() {
		wg.Wait()
		close(results)
	}()

	return results
}

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.

func SetupPipeLine(root string) error {
	done := make(chan struct{})
	defer close(done)

	// do the file walk
	paths, errc := walkFiles(done, root)

	// process the image
	resultImages := processImage(done, paths)

	// save thumbnail images
	results := saveThumbnail(done, resultImages)

	// save thumbnail images
	for r := range results {
		if r.err != nil {
			return r.err
		}
		fmt.Printf("%s -> %s\n", r.srcImagePath, r.destImagePath)
	}

	// check for error on the channel, from walkfiles stage.
	if err := <-errc; err != nil {
		return err
	}

	return nil
}

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.

// Image processing - sequential
// Input - directory with images.
// output - thumbnail images
func main() {
	if len(os.Args) < 2 {
		log.Fatal("need to send directory path of images")
	}
	start := time.Now()

	// using sequential
	// err := sequensial.WalkFiles(os.Args[1])

	// using pipeline pattern
	err := pipelinepattern.SetupPipeLine(os.Args[1])

	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Time taken: %s\n", time.Since(start))
}

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

go run main.go images

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

➜  learn-golang-generator-image-thumbnail git:(main) ✗ go run main.go images
images/sample-11.jpg -> thumbnails/sample-11.jpg
images/sample-10.jpg -> thumbnails/sample-10.jpg
images/sample-1.jpg -> thumbnails/sample-1.jpg
images/sample-12.jpg -> thumbnails/sample-12.jpg
images/sample-13.jpg -> thumbnails/sample-13.jpg
images/sample-14.jpg -> thumbnails/sample-14.jpg
images/sample-4.jpg -> thumbnails/sample-4.jpg
images/sample-2.jpg -> thumbnails/sample-2.jpg
images/sample-3.jpg -> thumbnails/sample-3.jpg
images/sample-5.jpg -> thumbnails/sample-5.jpg
images/sample-6.jpg -> thumbnails/sample-6.jpg
images/sample-7.jpg -> thumbnails/sample-7.jpg
images/sample-8.jpg -> thumbnails/sample-8.jpg
images/sample-9.jpg -> thumbnails/sample-9.jpg
Time 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.

comments powered by Disqus