|
| 1 | +""" |
| 2 | +Profiling script for formidable. |
| 3 | +
|
| 4 | +Exercises the main code paths: |
| 5 | +1. Form initialization (field discovery, cloning) |
| 6 | +2. Request data parsing (flat dict -> nested) |
| 7 | +3. Field setting & validation |
| 8 | +4. NestedForms (multiple sub-forms) |
| 9 | +5. SlugField (unicode normalization) |
| 10 | +6. HTML rendering |
| 11 | +
|
| 12 | +Run: |
| 13 | + uv run python profile_formidable.py |
| 14 | +
|
| 15 | +Outputs: |
| 16 | + - Console: top 40 cumulative-time entries |
| 17 | + - profile_results.prof: full cProfile dump (for snakeviz, etc.) |
| 18 | +""" |
| 19 | + |
| 20 | +import cProfile |
| 21 | +import pstats |
| 22 | + |
| 23 | +import formidable as f |
| 24 | + |
| 25 | + |
| 26 | +# -- Define realistic forms -------------------------------------------------- |
| 27 | + |
| 28 | +class AddressForm(f.Form): |
| 29 | + street = f.TextField() |
| 30 | + city = f.TextField() |
| 31 | + zip_code = f.TextField(required=False) |
| 32 | + country = f.TextField() |
| 33 | + |
| 34 | + |
| 35 | +class ContactForm(f.Form): |
| 36 | + name = f.TextField() |
| 37 | + email = f.EmailField() |
| 38 | + phone = f.TextField(required=False) |
| 39 | + age = f.IntegerField(required=False) |
| 40 | + website = f.URLField(required=False) |
| 41 | + slug = f.SlugField(required=False) |
| 42 | + subscribe = f.BooleanField(required=False) |
| 43 | + notes = f.TextField(required=False) |
| 44 | + |
| 45 | + address = f.FormField(AddressForm) |
| 46 | + addresses = f.NestedForms(AddressForm, min_items=1, max_items=10) |
| 47 | + |
| 48 | + |
| 49 | +# -- Build test data ---------------------------------------------------------- |
| 50 | + |
| 51 | +def make_flat_reqdata(n_addresses=5): |
| 52 | + """Simulate flat request data as it would arrive from an HTML form.""" |
| 53 | + data = { |
| 54 | + "name": "José García-López", |
| 55 | + "email": "jose@example.com", |
| 56 | + "phone": "+1-555-0123", |
| 57 | + "age": "42", |
| 58 | + "website": "https://example.com/~jose", |
| 59 | + "slug": "Héllo Wörld! Ça va très bien, merci αβγδ", |
| 60 | + "subscribe": "on", |
| 61 | + "notes": "Some notes here", |
| 62 | + # FormField (single nested) |
| 63 | + "address[street]": "123 Main St", |
| 64 | + "address[city]": "Springfield", |
| 65 | + "address[zip_code]": "62704", |
| 66 | + "address[country]": "US", |
| 67 | + } |
| 68 | + # NestedForms (multiple nested) |
| 69 | + for i in range(n_addresses): |
| 70 | + prefix = f"addresses[{i}]" |
| 71 | + data[f"{prefix}[street]"] = f"{100 + i} Oak Avenue" |
| 72 | + data[f"{prefix}[city]"] = f"City {i}" |
| 73 | + data[f"{prefix}[zip_code]"] = f"{10000 + i}" |
| 74 | + data[f"{prefix}[country]"] = "US" |
| 75 | + |
| 76 | + return data |
| 77 | + |
| 78 | + |
| 79 | +# -- Workloads ---------------------------------------------------------------- |
| 80 | + |
| 81 | +def workload_init_only(iterations=1000): |
| 82 | + """Measure form class instantiation (no data).""" |
| 83 | + for _ in range(iterations): |
| 84 | + ContactForm() |
| 85 | + |
| 86 | + |
| 87 | +def workload_parse_set_validate(iterations=500): |
| 88 | + """Measure parse + set + validate cycle.""" |
| 89 | + data = make_flat_reqdata(n_addresses=5) |
| 90 | + for _ in range(iterations): |
| 91 | + form = ContactForm(data) |
| 92 | + form.is_valid |
| 93 | + |
| 94 | + |
| 95 | +def workload_large_nested(iterations=100): |
| 96 | + """Measure with many nested forms.""" |
| 97 | + data = make_flat_reqdata(n_addresses=50) |
| 98 | + for _ in range(iterations): |
| 99 | + form = ContactForm(data) |
| 100 | + form.is_valid |
| 101 | + |
| 102 | + |
| 103 | +def workload_render_html(iterations=500): |
| 104 | + """Measure HTML rendering helpers.""" |
| 105 | + data = make_flat_reqdata(n_addresses=3) |
| 106 | + form = ContactForm(data) |
| 107 | + for _ in range(iterations): |
| 108 | + for field in form: |
| 109 | + field.label("Label") |
| 110 | + field.text_input() |
| 111 | + field.error_tag() |
| 112 | + |
| 113 | + |
| 114 | +def workload_slug(iterations=2000): |
| 115 | + """Measure slug field processing.""" |
| 116 | + from formidable.fields.slug import slugify |
| 117 | + texts = [ |
| 118 | + "Héllo Wörld! Ça va très bien", |
| 119 | + "αβγδεζηθικλμνξοπρστυφχψω", |
| 120 | + "The Quick Brown Fox Jumps Over The Lazy Dog 123!@#$%", |
| 121 | + "ñoño año español café résumé naïve", |
| 122 | + "ა ბ გ დ ე ვ ზ თ ი კ ლ მ ნ ო პ", |
| 123 | + ] |
| 124 | + for _ in range(iterations): |
| 125 | + for text in texts: |
| 126 | + slugify(text) |
| 127 | + |
| 128 | + |
| 129 | +def workload_parser(iterations=2000): |
| 130 | + """Measure raw parser performance.""" |
| 131 | + from formidable.parser import parse |
| 132 | + data = make_flat_reqdata(n_addresses=20) |
| 133 | + for _ in range(iterations): |
| 134 | + parse(data) |
| 135 | + |
| 136 | + |
| 137 | +def run_all(): |
| 138 | + """Run all workloads together for a combined profile.""" |
| 139 | + workload_init_only() |
| 140 | + workload_parse_set_validate() |
| 141 | + workload_large_nested() |
| 142 | + workload_render_html() |
| 143 | + workload_slug() |
| 144 | + workload_parser() |
| 145 | + |
| 146 | + |
| 147 | +# -- Main -------------------------------------------------------------------- |
| 148 | + |
| 149 | +if __name__ == "__main__": |
| 150 | + print("Profiling formidable...") |
| 151 | + print("=" * 70) |
| 152 | + |
| 153 | + profiler = cProfile.Profile() |
| 154 | + profiler.enable() |
| 155 | + run_all() |
| 156 | + profiler.disable() |
| 157 | + |
| 158 | + # Save for visualization tools (snakeviz, pyprof2calltree, etc.) |
| 159 | + profiler.dump_stats("profile_results.prof") |
| 160 | + |
| 161 | + # Print summary |
| 162 | + stats = pstats.Stats(profiler) |
| 163 | + stats.strip_dirs() |
| 164 | + stats.sort_stats("cumulative") |
| 165 | + |
| 166 | + print("\n TOP 40 BY CUMULATIVE TIME") |
| 167 | + print("=" * 70) |
| 168 | + stats.print_stats(40) |
| 169 | + |
| 170 | + print("\n TOP 30 BY TOTAL (SELF) TIME") |
| 171 | + print("=" * 70) |
| 172 | + stats.sort_stats("tottime") |
| 173 | + stats.print_stats(30) |
| 174 | + |
| 175 | + print(f"\nFull profile saved to: profile_results.prof") |
| 176 | + print("Visualize with: uv run snakeviz profile_results.prof") |
0 commit comments