github.com/utopiagio/gio@v0.0.8/widget/text_bench_test.go (about)

     1  package widget
     2  
     3  import (
     4  	"fmt"
     5  	"image"
     6  	"math/rand"
     7  	"os"
     8  	"sort"
     9  	"testing"
    10  
    11  	"github.com/utopiagio/gio/font/gofont"
    12  	"github.com/utopiagio/gio/gpu/headless"
    13  	"github.com/utopiagio/gio/io/system"
    14  	"github.com/utopiagio/gio/layout"
    15  	"github.com/utopiagio/gio/op"
    16  	"github.com/utopiagio/gio/text"
    17  	"github.com/utopiagio/gio/unit"
    18  
    19  	colEmoji "eliasnaur.com/font/noto/emoji/color"
    20  	"github.com/utopiagio/gio/font"
    21  	"github.com/utopiagio/gio/font/gofont"
    22  	"github.com/utopiagio/gio/font/opentype"
    23  	"github.com/utopiagio/gio/gpu/headless"
    24  	"github.com/utopiagio/gio/io/system"
    25  	"github.com/utopiagio/gio/layout"
    26  	"github.com/utopiagio/gio/op"
    27  	"github.com/utopiagio/gio/text"
    28  	"github.com/utopiagio/gio/unit"
    29  	"golang.org/x/exp/maps"
    30  )
    31  
    32  var (
    33  	documents = map[string]string{
    34  		"latin":   latinDocument,
    35  		"arabic":  arabicDocument,
    36  		"complex": complexDocument,
    37  		"emoji":   emojiDocument,
    38  	}
    39  	emojiFace = func() opentype.Face {
    40  		face, _ := opentype.Parse(colEmoji.TTF)
    41  		return face
    42  	}()
    43  	sizes      = []int{10, 100, 1000}
    44  	locales    = []system.Locale{arabic, english}
    45  	benchFonts = func() []font.FontFace {
    46  		collection := gofont.Collection()
    47  		collection = append(collection, arabicCollection...)
    48  		collection = append(collection, font.FontFace{
    49  			Font: font.Font{
    50  				Typeface: "Noto Color Emoji",
    51  			},
    52  			Face: emojiFace,
    53  		})
    54  		return collection
    55  	}()
    56  )
    57  
    58  func runBenchmarkPermutations(b *testing.B, benchmark func(b *testing.B, runes int, locale system.Locale, document string)) {
    59  	docKeys := maps.Keys(documents)
    60  	sort.Strings(docKeys)
    61  	for _, locale := range locales {
    62  		for _, runes := range sizes {
    63  			for _, textType := range docKeys {
    64  				txt := documents[textType]
    65  				b.Run(fmt.Sprintf("%drunes-%s-%s", runes, locale.Direction, textType), func(b *testing.B) {
    66  					benchmark(b, runes, locale, txt)
    67  				})
    68  			}
    69  		}
    70  	}
    71  }
    72  
    73  var render bool
    74  
    75  func init() {
    76  	if _, ok := os.LookupEnv("RENDER_WIDGET_TESTS"); ok {
    77  		render = true
    78  	}
    79  }
    80  
    81  func BenchmarkLabelStatic(b *testing.B) {
    82  	runBenchmarkPermutations(b, func(b *testing.B, runeCount int, locale system.Locale, txt string) {
    83  		var win *headless.Window
    84  		size := image.Pt(200, 1000)
    85  		gtx := layout.Context{
    86  			Ops: new(op.Ops),
    87  			Constraints: layout.Constraints{
    88  				Max: size,
    89  			},
    90  			Locale: locale,
    91  		}
    92  		cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
    93  		if render {
    94  			win, _ = headless.NewWindow(size.X, size.Y)
    95  			defer win.Release()
    96  		}
    97  		fontSize := unit.Sp(10)
    98  		font := font.Font{}
    99  		runes := []rune(txt)[:runeCount]
   100  		runesStr := string(runes)
   101  		l := Label{}
   102  		b.ResetTimer()
   103  		for i := 0; i < b.N; i++ {
   104  			l.Layout(gtx, cache, font, fontSize, runesStr, op.CallOp{})
   105  			if render {
   106  				win.Frame(gtx.Ops)
   107  			}
   108  			gtx.Ops.Reset()
   109  		}
   110  	})
   111  }
   112  
   113  func BenchmarkLabelDynamic(b *testing.B) {
   114  	runBenchmarkPermutations(b, func(b *testing.B, runeCount int, locale system.Locale, txt string) {
   115  		var win *headless.Window
   116  		size := image.Pt(200, 1000)
   117  		gtx := layout.Context{
   118  			Ops: new(op.Ops),
   119  			Constraints: layout.Constraints{
   120  				Max: size,
   121  			},
   122  			Locale: locale,
   123  		}
   124  		cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
   125  		if render {
   126  			win, _ = headless.NewWindow(size.X, size.Y)
   127  			defer win.Release()
   128  		}
   129  		fontSize := unit.Sp(10)
   130  		font := font.Font{}
   131  		runes := []rune(txt)[:runeCount]
   132  		l := Label{}
   133  		r := rand.New(rand.NewSource(42))
   134  		b.ResetTimer()
   135  		for i := 0; i < b.N; i++ {
   136  			// simulate a constantly changing string
   137  			a := r.Intn(len(runes))
   138  			b := r.Intn(len(runes))
   139  			runes[a], runes[b] = runes[b], runes[a]
   140  			l.Layout(gtx, cache, font, fontSize, string(runes), op.CallOp{})
   141  			if render {
   142  				win.Frame(gtx.Ops)
   143  			}
   144  			gtx.Ops.Reset()
   145  		}
   146  	})
   147  }
   148  
   149  func BenchmarkEditorStatic(b *testing.B) {
   150  	runBenchmarkPermutations(b, func(b *testing.B, runeCount int, locale system.Locale, txt string) {
   151  		var win *headless.Window
   152  		size := image.Pt(200, 1000)
   153  		gtx := layout.Context{
   154  			Ops: new(op.Ops),
   155  			Constraints: layout.Constraints{
   156  				Max: size,
   157  			},
   158  			Locale: locale,
   159  		}
   160  		cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
   161  		if render {
   162  			win, _ = headless.NewWindow(size.X, size.Y)
   163  			defer win.Release()
   164  		}
   165  		fontSize := unit.Sp(10)
   166  		font := font.Font{}
   167  		runes := []rune(txt)[:runeCount]
   168  		runesStr := string(runes)
   169  		e := Editor{}
   170  		e.SetText(runesStr)
   171  		b.ResetTimer()
   172  		for i := 0; i < b.N; i++ {
   173  			e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   174  			if render {
   175  				win.Frame(gtx.Ops)
   176  			}
   177  			gtx.Ops.Reset()
   178  		}
   179  	})
   180  }
   181  
   182  func BenchmarkEditorDynamic(b *testing.B) {
   183  	runBenchmarkPermutations(b, func(b *testing.B, runeCount int, locale system.Locale, txt string) {
   184  		var win *headless.Window
   185  		size := image.Pt(200, 1000)
   186  		gtx := layout.Context{
   187  			Ops: new(op.Ops),
   188  			Constraints: layout.Constraints{
   189  				Max: size,
   190  			},
   191  			Locale: locale,
   192  		}
   193  		cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
   194  		if render {
   195  			win, _ = headless.NewWindow(size.X, size.Y)
   196  			defer win.Release()
   197  		}
   198  		fontSize := unit.Sp(10)
   199  		font := font.Font{}
   200  		runes := []rune(txt)[:runeCount]
   201  		e := Editor{}
   202  		e.SetText(string(runes))
   203  		r := rand.New(rand.NewSource(42))
   204  		b.ResetTimer()
   205  		for i := 0; i < b.N; i++ {
   206  			// simulate a constantly changing string
   207  			a := r.Intn(e.Len())
   208  			b := r.Intn(e.Len())
   209  			e.SetCaret(a, a+1)
   210  			takeStr := e.SelectedText()
   211  			e.Insert("")
   212  			e.SetCaret(b, b)
   213  			e.Insert(takeStr)
   214  			e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   215  			if render {
   216  				win.Frame(gtx.Ops)
   217  			}
   218  			gtx.Ops.Reset()
   219  		}
   220  	})
   221  }
   222  
   223  func FuzzEditorEditing(f *testing.F) {
   224  	f.Add(complexDocument, int16(0), int16(len([]rune(complexDocument))))
   225  	gtx := layout.Context{
   226  		Ops: new(op.Ops),
   227  		Constraints: layout.Constraints{
   228  			Max: image.Pt(200, 1000),
   229  		},
   230  		Locale: arabic,
   231  	}
   232  	cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts))
   233  	fontSize := unit.Sp(10)
   234  	font := font.Font{}
   235  	e := Editor{}
   236  	f.Fuzz(func(t *testing.T, txt string, replaceFrom, replaceTo int16) {
   237  		e.SetText(txt)
   238  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   239  		// simulate a constantly changing string
   240  		if e.Len() > 0 {
   241  			a := int(replaceFrom) % e.Len()
   242  			b := int(replaceTo) % e.Len()
   243  			e.SetCaret(a, a+1)
   244  			takeStr := e.SelectedText()
   245  			e.Insert("")
   246  			e.SetCaret(b, b)
   247  			e.Insert(takeStr)
   248  		}
   249  		e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
   250  		gtx.Ops.Reset()
   251  	})
   252  }
   253  
   254  const (
   255  	latinDocument = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
   256  Porttitor eget dolor morbi non arcu risus quis.
   257  Nibh sit amet commodo nulla.
   258  Posuere ac ut consequat semper viverra nam libero justo.
   259  Risus in hendrerit gravida rutrum quisque.
   260  Natoque penatibus et magnis dis parturient montes nascetur.
   261  In metus vulputate eu scelerisque felis imperdiet proin fermentum.
   262  Mattis rhoncus urna neque viverra.
   263  Elit pellentesque habitant morbi tristique.
   264  Nisl nunc mi ipsum faucibus vitae aliquet nec.
   265  Sed augue lacus viverra vitae congue eu consequat.
   266  At quis risus sed vulputate odio ut.
   267  Sit amet volutpat consequat mauris nunc congue nisi.
   268  Dignissim cras tincidunt lobortis feugiat.
   269  Faucibus turpis in eu mi bibendum.
   270  Odio aenean sed adipiscing diam donec adipiscing tristique.
   271  Fermentum leo vel orci porta non pulvinar.
   272  Ut venenatis tellus in metus vulputate eu scelerisque felis imperdiet.
   273  Et netus et malesuada fames ac turpis.
   274  Venenatis urna cursus eget nunc scelerisque viverra mauris in.
   275  Risus ultricies tristique nulla aliquet enim tortor.
   276  Risus pretium quam vulputate dignissim suspendisse in.
   277  Interdum velit euismod in pellentesque massa placerat duis ultricies lacus.
   278  Proin gravida hendrerit lectus a.
   279  Auctor augue mauris augue neque gravida in fermentum et.
   280  Laoreet sit amet cursus sit amet dictum.
   281  In fermentum et sollicitudin ac orci phasellus egestas tellus rutrum.
   282  Tempus imperdiet nulla malesuada pellentesque elit eget gravida.
   283  Consequat id porta nibh venenatis cras sed.
   284  Vulputate ut pharetra sit amet aliquam.
   285  Congue mauris rhoncus aenean vel elit.
   286  Risus quis varius quam quisque id diam vel quam elementum.
   287  Pretium lectus quam id leo in vitae.
   288  Sed sed risus pretium quam vulputate dignissim suspendisse in est.
   289  Velit laoreet id donec ultrices.
   290  Nunc sed velit dignissim sodales ut.
   291  Nunc scelerisque viverra mauris in aliquam sem fringilla ut.
   292  Sed enim ut sem viverra aliquet eget sit.
   293  Convallis posuere morbi leo urna molestie at.
   294  Aliquam id diam maecenas ultricies mi eget mauris.
   295  Ipsum dolor sit amet consectetur adipiscing elit ut aliquam.
   296  Accumsan tortor posuere ac ut consequat semper.
   297  Viverra vitae congue eu consequat ac felis donec et odio.
   298  Scelerisque in dictum non consectetur a.
   299  Consequat nisl vel pretium lectus quam id leo in vitae.
   300  Morbi tristique senectus et netus et malesuada fames ac turpis.
   301  Ac orci phasellus egestas tellus.
   302  Tempus egestas sed sed risus.
   303  Ullamcorper morbi tincidunt ornare massa eget egestas purus.
   304  Nibh venenatis cras sed felis eget velit.`
   305  	arabicDocument = `و سأعرض مثال حي لهذا، من منا لم يتحمل جهد بدني شاق إلا من أجل الحصول على ميزة أو فائدة؟ ولكن من لديه الحق أن ينتقد شخص ما أراد أن يشعر بالسعادة التي لا تشوبها عواقب أليمة أو آخر أراد أن يتجنب الألم الذي ربما تنجم عنه بعض المتعة ؟ علي الجانب الآخر نشجب ونستنكر هؤلاء الرجال المفتونون بنشوة اللحظة الهائمون في رغباتهم فلا يدركون ما يعقبها من الألم والأسي المحتم، واللوم كذلك يشمل هؤلاء الذين أخفقوا في واجباتهم نتيجة لضعف إرادتهم فيتساوي مع هؤلاء الذين يتجنبون وينأون عن تحمل الكدح والألم .
   306  من المفترض أن نفرق بين هذه الحالات بكل سهولة ومرونة.
   307  في ذاك الوقت عندما تكون قدرتنا علي الاختيار غير مقيدة بشرط وعندما لا نجد ما يمنعنا أن نفعل الأفضل فها نحن نرحب بالسرور والسعادة ونتجنب كل ما يبعث إلينا الألم.
   308  في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور بالسرور ونقبل ما يجلبه إلينا الأسى.
   309  الإنسان الحكيم عليه أن يمسك زمام الأمور ويختار إما أن يرفض مصادر السعادة من أجل ما هو أكثر أهمية أو يتحمل الألم من أجل ألا يتحمل ما هو أسوأ.
   310  و سأعرض مثال حي لهذا، من منا لم يتحمل جهد بدني شاق إلا من أجل الحصول على ميزة أو فائدة؟ ولكن من لديه الحق أن ينتقد شخص ما أراد أن يشعر بالسعادة التي لا تشوبها عواقب أليمة أو آخر أراد أن يتجنب الألم الذي ربما تنجم عنه بعض المتعة ؟ علي الجانب الآخر نشجب ونستنكر هؤلاء الرجال المفتونون بنشوة اللحظة الهائمون في رغباتهم فلا يدركون ما يعقبها من الألم والأسي المحتم، واللوم كذلك يشمل هؤلاء الذين أخفقوا في واجباتهم نتيجة لضعف إرادتهم فيتساوي مع هؤلاء الذين يتجنبون وينأون عن تحمل الكدح والألم .
   311  من المفترض أن نفرق بين هذه الحالات بكل سهولة ومرونة.
   312  في ذاك الوقت عندما تكون قدرتنا علي الاختيار غير مقيدة بشرط وعندما لا نجد ما يمنعنا أن نفعل الأفضل فها نحن نرحب بالسرور والسعادة ونتجنب كل ما يبعث إلينا الألم.
   313  في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور بالسرور ونقبل ما يجلبه إلينا الأسى.
   314  الإنسان الحكيم عليه أن يمسك زمام الأمور ويختار إما أن يرفض مصادر السعادة من أجل ما هو أكثر أهمية أو يتحمل الألم من أجل ألا يتحمل ما هو أسوأ.`
   315  	complexDocument = `و سأعرض مثال dolor sit amet, لم يتحمل جهد adipiscing elit, sed do الحصول على ميزة incididunt ut labore أن ينتقد magna aliqua.
   316  Porttitor إرادتهم فيتساوي morbi non arcu يدركون ما يعقبها .
   317  Nibh نشجب ونستنكر commodo nulla.
   318  بكل سهولة ومرونة ut consequat  لهذا، من منا  nam libero justo.
   319  Risus in hendrerit علينا الواجب والعمل.
   320  Natoque تكون قدرتنا علي magnis dis parturient  يمسك زمام الأمور ويختار.
   321  In نجد ما يمنعنا eu scelerisque ونظراً للالتزامات التي fermentum.
   322  Mattis ة بشرط وعندما لا  neque viverra.
   323  يمسك زمام الأمور  habitant لهذا، من.
   324  Nisl تي يفرضها علينا faucibus ،من منا لم nec.
   325  Sed augue علي الاختيار غير vitae congue eu consequat.
   326  At quis risus سك زمام الأمور ويختار.
   327  Sit amet volutpat consequat mauris الأمور ويختار إما nisi.
   328  Dignissim لواجب والعمل tincidunt سنتنازل feugiat.
   329  Faucibus التزامات in eu mi bibendum.
   330  Odio ويختار إما أن يرفض مصادر السعادة sed adipiscing ذا، من منا لم  tristique.
   331  Fermentum leo vel ور ويختار إما  pulvinar.
   332  Ut ر إما أن يرفض مصادر السعادة من in metus  تكون قدرتنا علي  felis imperdiet.
   333  ي الاختيار غير مقيدة بشرط et malesuada fames ac turpis.
   334  Venenatis على ميزة أو فائدة؟ ولكن  eget nunc scelerisque سك زمام الأمور ويختار إما in.
   335  رتنا ultricies tristique ي الاختيار غير مقيدة بشرط enim tortor.
   336  Risus اختيار غير مقيدة بشرط وعندما  quam سان الحكيم عليه أن  suspendisse in.
   337  Interdum velit  ونظراً للالتزامات التي  pellentesque massa placerat لأمور ويختار إما أن يرفض  lacus.
   338  Proin دما تكون قدرتنا علي الاختيار  lectus a.
   339  Auctor  الوقت عندما تكون augue neque ض مثال حي  fermentum et.
   340  Laoreet مسك زمام الأمور ويختار  amet cursus  لم يتحمل جهد  dictum.
   341  In fermentum et sollicitudin ac orci phasellus  علي الاختيار غير  rutrum.
   342  Tempus imperdiet  المفترض أن نفرق  pellentesque ت بكل سهولة eget gravida.
   343  Consequat id portaمصادر السعادة  cras sed.
   344  Vulputate علي الاختيار غير مقيدة sit amet aliquam.
   345  Congue mauris حيان ونظراً للالتزامات التي vel elit.
   346  Risus quis varius quam quisque id ار غير مقيدة بشرط elementum.
   347  Pretium تي يفرضها علينا الواجب leo in vitae.
   348   شاق إلا من أجل pretium quam الحكيم عليه أن يمسك  suspendisse in est.
   349  Velit ونظراً للالتزامات التي يفرضها ultrices.
   350   الوقت عندما تكون  velit dignissim يه أن يمسك .
   351  Nunc scelerisque viverra mauris in aliquam sem ر إما أن  ut.
   352  السعادة من أجل ما هو أكثر أهمية أو يتحمل الألم
   353  Convallis posuere morbi leo urna molestie at.`
   354  	emojiDocument = `📚🎶🐰🌷👹🌟 🔰🐲📑🍢🔎 👢💮👷👧💑🐪 📙📜🐎🏠🎠 👧🌼💛🎉💜🎍 🔜💷🐉👘🕟📗🍟 🎆🍚📹💄 🐾🎩💽👘 📒💕👅💽🐩 📷🌌🌚🎣📌. 🍈🍅🔖🍄 🍐🔈🍤🐽 🐹💘🍚👩📡 🎸🏠🔳🏩🌳💣 🔡🔠🕤🔔🎴📕 📼👝🎓🕗💸 📓🌽🍟💵🕗🌒🏉📨 🔀🏉🍴💘🍣💸 🔪🔻🕖🎰 🐲👮🔙🌇🐒🏇 🐝🌚🏫🔀👍 👾🎧🍋🍔👧 💣💞🐴👆🐢🏊📀 🕤🌃🍌🕛🔬. 🏃🍜🍔🐽🎁🏩🎰 📮🍄🐖💕👈 🔠🕡🐊💞🍬📳 🎤🌆🌛🐍🔳 🐄🔇🔱🌇📺👞 💌👍📳🎤🏂 👞🎉🍶📊🔶🌅🐭🕙 🍜📠🎴💒🔶 📀💂🌷👺👙.
   355  
   356  📥🕝🎎🐻💘🍇🔤 💠🎇📦👩🍁 👜🍏🔏👎 🔟🌹🌗🎬🔙 🐁📛🐝🐏🐣 🔃🗻🔎🌺👀 📰📮🏩👯🐳🍀🍇 🍨📵🌂📌 👌📐🏨🐉 🍏🍘🔟🎣🔏📠 👤📭🐱📣. 🕓👶🎳📭🔌📃🔧 📟🔰🌂🎈🔣 🔤👍🍤👔🐪 🔨🎼🎊🎪🕝🐬 📴🎶🔈🔐🔘 🐬🐯🕜🎎👴🎃 🎑🐾👏👇🔭 🐥🔙💦🔩🔮 👊🐶👗📕 🐎📹👠🍤 🔢💘📷🐷🐂 🐫💕🕕🍖🔆🎽 👼🎶🌸👻🔷🌰 🔔💉💱🔂👵🔑. 🌁🎪🎌🍘🍏 🌛🍂🔎🕃📧👻 🎍🌔🐦🐻 🔉🎌🌘💉👒 📙💠🔙📰 🌒👏💪🌇💈 🌌📯📂🌀🔁 🏧💷🍀🐐🏈 📢🌏🔷💭 👋🕓🌓🕛🏢👡👋 🍶🐂🍠🔟 👵🏇🔶🕜👎. 👹💉🔌🍳🕗 🐫🌈🔠🐀🎩🎽 👺🔣🔂👪👴🐚🕙 👀🕓🔱🌇 🎻🐘🔐🕕 🌉🔡🐊🍮 💫🎆🎹🐍🎯 🐑🐱🍠🕑🍒.
   357  
   358  🎳🐎🔹🎾🐹📖 📘🐒📷🕧🔛 🐾📺🎿🍖💂🕥 🍜🎷🍣👳 🕛📧💶🌑 🌀💣🎎🐛🎪🐒 🎇🌹👺🎆💄📚 🔓🍗📓🎂🌍🌘📢 🍩💞🏂💥🔹📇💴 🐇🕝💹🐣💔🎫 👐🍼🏰🎄🎨👚 👑🔗🍅🐈 🐰🐙🌻👹👆 👬🐧🍬🕡🐽💉 🌅🔉🎤🔁📨🔧🔀🍏 🎼🔛📉🌺. 👖🌔🍢🏂💯 🏁🐰🍉📬🍖 📨💜📮🔕🎣🔩 🔏🕀🏫🎳📵👭👟 💨💃📶🎃 📚🔇🍛👽🐍🐄 🔼👻🍮🍔🍨🏪🐺 📩📜🍨📖 🏢🎉🔢🌚🌀🔊💍 🐟🕚🔴🎿🍞 🌈📤👲🌿🌅🍲📛💍 🐦🔰🐗🐆🎻 👑🕐📔🎁🍙🔪🔭. 🎐🐵🎼🌒🎰🍳🎽 🐻🔉💺🕁🍷 🐛🍬💦📶🔖 🔕🌳💃🌺🔢 💒📒🔘🐸👩 🌺🍈🌀🏁🎢🔖 📈🎸🐖👪 🐅🏁🔹🎬🍖📊🗼 🎬📅💝📀🎐. 🌗📍👇🎠 🌸🐸🐐🍕🐋 💈🌌🐶💤🌻🐞 🍯🌳📌🍮🐻🍝 🕦📯🔱👒 💖🌱🐨🎰🏭🏈 🔳🏩🌟🔭📢📒 🔅💬💓💻💁💂 🔗🍂🏇🌒🌂💩🕢 🔙🌆💞📜🔘👇 🍎🌃🔢🌵🏬 🔄💢🍨📋💇🌄 🍝🍧💂🏮🏁. 🎬🐽🔇🎣🌜🔣 🌍🔒👿🎆🌞🍇🍸 👖🍘🏡🕣 📝🐖💆🎈 👙🔳👙🔩👀🔂 🐤📈💃👗🔌🎾🔭🍴 🌺👛🌵🌕🐺 🎆💼👌👘🍈👛 🎳🐪🕧🏄 💯🍟💂👖🎍 🕀💟🌷💕🐉🐲🎷. 🎍👂📓🌽 🐉🕕🐤🌲📟🔂💷 🎑📛🕠🔹🐚 🍆📹🐚🐵🏇🏢 🍠💱🕦💙🏢🐌🎎 🐄🍨📄🌾🎻🏈 🏇🍪💸🔆💍📢👢 💇🌋👝🕜🌍🐶🎓 🍪📄🐤🎃💖 🔲🕒🍧🌎🐪🌶 🍓👲🔭🍯🌔👌 🔼🐗🗼🍂 🔶🍯🎶🐅🐂💗🐴🐶 📭📰📔👬🏯🕟🐄🍊 💆👞📆🐶🌖🎁👺 🐃💺👊🌿🎌.
   359  
   360  🍧🕔👆🔭🕛👇 🐆🔖🎂🐭📗🗼🐐 🌐🎢🌞💛🐚 🌿🎶💎💬🔩 💾🔐🎷🍙🐬🕐 🌏🍄🎾🐎🌽🍓🐳 💥🍎👳📫🐤📼🎾 👨🕃🕞🍯🍲. 💥🎍🔉🎈👻🔵🏬🔸 🔼🍹🔱🔮🕔 🌈💎👜📠 👢🍻🍢🎃👺🌍👰 🍵👃🕠🍎🍑 📜💥📘📌 🔹🔵🍷👅💏 💮💘🐜📠👬📖 🌅🍺🔇🌈👒🔀 🎢🌆💌🍬📱🎰 🌺🍆🔰🏁🍁🎠 🔇🔁🌹🔞🎀🎬🐭🌹 🏬📫🗾🎻📌. 🐠🏣👋👊🐟 👲🔣💻👅🎎 🎇🌲🕑🍨📯 🐜📵💙📷🎒🕔 🎇🏀🔴🐑🌗 🎧🔡👅🕁🏉👛🐬 🕧🐞🎩📓🍆📪 🐼📻👼🌄 🌟🌺🏦🍧🍕🐯 🕕🕦🐤💆🍧💩 🐑📜👏👐🐧🍞👵 👞🌲🍼🔍 🌛🐔🌄🎸🐯.`
   361  )