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