[{"data":1,"prerenderedAt":700},["ShallowReactive",2],{"/en-us/blog/making-gitlab-faster/":3,"navigation-en-us":32,"banner-en-us":449,"footer-en-us":461,"Yorick Peterse":672,"next-steps-en-us":685},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"seo":8,"content":16,"config":22,"_id":25,"_type":26,"title":27,"_source":28,"_file":29,"_stem":30,"_extension":31},"/en-us/blog/making-gitlab-faster","blog",false,"",{"title":9,"description":10,"ogTitle":9,"ogDescription":10,"noIndex":6,"ogImage":11,"ogUrl":12,"ogSiteName":13,"ogType":14,"canonicalUrls":12,"schema":15},"Making GitLab Faster","In GitLab 8.5 we shipped numerous performance improvements. In this article we'll take a look at some of these changes and the process involved in finding and resolving these issues.","https://res.cloudinary.com/about-gitlab-com/image/upload/v1749663397/Blog/Hero%20Images/logoforblogpost.jpg","https://about.gitlab.com/blog/making-gitlab-faster","https://about.gitlab.com","article","\n                        {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"Article\",\n        \"headline\": \"Making GitLab Faster\",\n        \"author\": [{\"@type\":\"Person\",\"name\":\"Yorick Peterse\"}],\n        \"datePublished\": \"2016-02-25\",\n      }",{"title":9,"description":10,"authors":17,"heroImage":11,"date":19,"body":20,"category":21},[18],"Yorick Peterse","2016-02-25","\nIn GitLab 8.5 we shipped numerous performance improvements. In this article\nwe'll take a look at some of these changes and the process involved in finding\nand resolving these issues. In particular we'll look at the following merge\nrequests:\n\n* [Optimize fetching issues closed by a merge request][mr2625]\n* [Improve performance of retrieving last update times for events][mr2613]\n* [Only set autocrlf when creating/updating files][mr2859]\n\n\u003C!--more-->\n\n## Performance Monitoring & Tooling\n\nWithout a proper production performance monitoring system and a good set of\ntools it's nearly impossible to find and resolve performance problems. GitLab\ncomes with two systems to make it possible to measure application performance:\n\n* [GitLab Performance Monitoring][monitoring]: a monitoring system using\n  [InfluxDB][influxdb] to track application performance of production\n  environments (though you can also use it during development). Data is then\n  visualized using [Grafana][grafana], though users can use any software capable\n  of extracting data from InfluxDB.\n* Sherlock: a development only monitoring system. Due to the overhead of\n  Sherlock it's not suitable for production environments. For example, Sherlock\n  uses [rblineprof][rblineprof] to track execution timings on a per line basis\n  but this adds quite a bit of overhead.\n\nAnother very useful library is [benchmark-ips][benchmark-ips]. This library can\nbe used to measure the performance of snippets of code while taking care of\nwarming up any caches, Just In Time compilers, etc. For more information see the\n[benchmark-ips README][benchmark-ips-readme].\n\n### Limitations of Benchmarks\n\nWhile we're on the topic of benchmarks it's worth mentioning that benchmarks are\nonly really useful to see the impact of a certain change. For example, if\nbenchmark X can run Y iterations in a certain time period this gives you no\ninsight in how this will perform in a production environment; all it indicates\nis that it can run a certain number of iterations. However, when a certain\nchange results in the benchmark now completing twice as fast things start\ngetting interesting. While we still don't really know how the change will affect\nour production environment we at least know that in the most ideal case\nperformance will be twice as fast.\n\nIn short, just benchmarks aren't enough; you always have to measure (and _keep_\nmeasuring) the performance of code in a production environment. This may seem\nlike common knowledge but a few too many projects out there make bold claims\nabout their performance based solely on a set of benchmarks.\n\nWith that out of the way, let's get started.\n\n## Optimize fetching issues closed by a merge request\n\nCommit messages can be used to automatically close issues by adding the text\n\"Fixes #X\" or \"Closes #X\" to a commit message (where X refers to an issue ID).\nIn turn each merge request shows the list of issues that will be closed whenever\nthe merge request is merged. The description of a merge request can also include\ninclude text such as \"Fixes #X\" to close issues. In other words, the list of\nissues to close is a set composed out of the issues to close as extracted from\nthe commit messages and the issues to close as extracted from the merge\nrequest's description.\n\nWhich brings us to the method `MergeRequest#closes_issues`. This method is used\nto return the list of issues to close (as an Array of `Issue` instances). If we\nlook at the performance of this method over time we see the following:\n\n![MergeRequest#closes_issues Timings][mr2625-timings]\n\nThe small gap at the start of the graph is due to monitoring data only being\nretained for 30 days.\n\nTo summarize the timings:\n\n* A mean of around 500 milliseconds\n* A 95th percentile between 1 and 1.5 seconds\n* A 99th percentile between 1.5 and 2 seconds\n\n2 seconds (in the worst case) to retrieve a list of issues to close is not\nacceptable so it was clear there was some work to be done.\n\nPrior to 8.5 this method was implemented as the following:\n\n    def closes_issues(current_user = self.author)\n       if target_branch == project.default_branch\n         issues = commits.flat_map { |c| c.closes_issues(current_user) }\n         issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user).\n                    closed_by_message(description))\n         issues.uniq(&:id)\n       else\n         []\n       end\n    end\n\nWhen the target branch of a merge request equals the project's default branch\nthis method takes the following steps:\n\n1. For every commit in the merge request, grab the issues that should be closed\n   when the merge request is merged.\n2. Append the list of issues to close based on the merge request's description\n   to the list of issues created in step 1.\n3. Remove any duplicate issues (based on the issue IDs) from the resulting list.\n\nWhat stood out here is the following line:\n\n    issues = commits.flat_map { |c| c.closes_issues(current_user) }\n\nFor every commit the method `Commit#closes_issues` would be called, which in\nturn was implemented as the following:\n\n    def closes_issues(current_user = self.committer)\n      Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)\n    end\n\nFurther digging revealed that `Gitlab::ClosingIssueExtractor#closed_by_message`\nwould perform two steps:\n\n1. Extract the referenced issue IDs from a String\n2. Run a database query to return a list of corresponding `Issue` objects\n\nNote that the above steps would be performed for _every_ commit in a merge\nrequest, regardless of whether a commit would actually reference an issue or\nnot. As such the more commits a merge request would contain the slower things\nwould get.\n\nIf we look at how `Gitlab::ClosingIssueExtractor#closed_by_message` is\nimplemented and used we see that it operates on a single String and doesn't\nreally care what it contains or where it comes from as long as it contains\nreferences to issue IDs:\n\n    def closed_by_message(message)\n      return [] if message.nil?\n\n      closing_statements = []\n      message.scan(ISSUE_CLOSING_REGEX) do\n        closing_statements \u003C\u003C Regexp.last_match[0]\n      end\n\n      @extractor.analyze(closing_statements.join(\" \"))\n\n      @extractor.issues\n    end\n\nThis got me thinking: what if we concatenate all commit messages together and\npass the resulting String to `Gitlab::ClosingIssueExtractor#closed_by_message`?\nDoing so would mean performance is no longer affected by the amount of commits\nin a merge request.\n\nTo test this I wrote a benchmark to compare the old setup versus the idea I was\ngoing for:\n\n    require 'benchmark/ips'\n\n    project = Project.find_with_namespace('gitlab-org/gitlab-ce')\n    user    = User.find_by_username('yorickpeterse')\n    commits = ['Fixes #1', 'Fixes #2', 'Fixes #3']\n    desc    = 'This MR fixes #1 #2 #3'\n\n    Benchmark.ips do |bench|\n      # A somewhat simplified version of the old code (excluding any actual\n      # commit/merge request objects).\n      bench.report 'old' do\n        issues = commits.flat_map do |message|\n          Gitlab::ClosingIssueExtractor.new(project, user).\n            closed_by_message(message)\n        end\n\n        issues.push(*Gitlab::ClosingIssueExtractor.new(project, user).\n                   closed_by_message(desc))\n\n        issues.uniq(&:id)\n      end\n\n      # The new code\n      bench.report 'new' do\n        messages = commits + [desc]\n\n        Gitlab::ClosingIssueExtractor.new(project, user).\n          closed_by_message(messages.join(\"\\n\"))\n      end\n\n      bench.compare!\n    end\n\nWhen running this benchmark we get the following output:\n\n    Calculating -------------------------------------\n                     old     1.000  i/100ms\n                     new     1.000  i/100ms\n    -------------------------------------------------\n                     old      1.377  (± 0.0%) i/s -      7.000\n                     new      2.807  (± 0.0%) i/s -     15.000  in   5.345900s\n\n    Comparison:\n                     new:        2.8 i/s\n                     old:        1.4 i/s - 2.04x slower\n\nSo in this benchmark alone the new code is around 2 times faster than the old\ncode. The actual number of iterations isn't very relevant, we just want to know\nif we're on the right track or not.\n\nRunning the test suite showed no tests were broken by these changes so it was\ntime to set up a merge request and deploy this to GitLab.com (and of course\ninclude it in the next release, 8.5 in this case) to see the impact in a\nproduction environment. The merge request for this was [\"Optimize fetching\nissues closed by a merge request\"][mr2625]. These changes were deployed around\nthe 12th of February and we can see the impact on GitLab.com in the following\ngraph:\n\n![MergeRequest#closes_issues Timings][mr2625-timings]\n\nThat's right, we went from timings between 0.5 and 2.5 seconds to timings of\nless than 15 milliseconds (method call timings below 15 milliseconds are not\ntracked). Ship it!\n\n## Improve performance of retrieving last update times for events\n\nFor certain activity feeds we provide Atom feeds that users can subscribe to.\nFor example \u003Chttps://gitlab.com/yorickpeterse.atom> provides an Atom feed of\nmy public GitLab.com activity. The feed is built by querying a list of records\nfrom the database called \"events\". The SQL query is rather large as the list of\nevents to return is based on the projects a user has access to (in case of user\nactivity feeds). For example, for my own user profile the query would be as\nfollowing:\n\n    SELECT events.*\n    FROM events\n    LEFT OUTER JOIN projects ON projects.id = events.project_id\n    LEFT OUTER JOIN namespaces ON namespaces.id = projects.namespace_id\n    WHERE events.author_id IS NOT NULL\n    AND events.author_id = 209240\n    AND (\n        projects.id IN (\n            SELECT projects.id\n            FROM projects\n            WHERE projects.id IN (\n                -- All projects directly owned by a user.\n                SELECT projects.id\n                FROM projects\n                INNER JOIN namespaces ON projects.namespace_id = namespaces.id\n                WHERE namespaces.owner_id = 209240\n                AND namespaces.type IS NULL\n\n                UNION\n\n                -- All projects of the groups a user is a member of\n                SELECT projects.id\n                FROM projects\n                INNER JOIN namespaces ON projects.namespace_id = namespaces.id\n                INNER JOIN members ON namespaces.id = members.source_id\n                WHERE namespaces.type IN ('Group')\n                AND members.type IN ('GroupMember')\n                AND members.source_type = 'Namespace'\n                AND members.user_id = 209240\n\n                UNION\n\n                -- All projects (that don't belong to one of the groups of a\n                -- user) a user is a member of\n                SELECT projects.id\n                FROM projects\n                INNER JOIN members ON projects.id = members.source_id\n                WHERE members.type IN ('ProjectMember')\n                AND members.source_type = 'Project'\n                AND members.user_id = 209240\n            )\n\n            UNION\n\n            -- All publicly available projects, regardless of whether we still\n            -- have access or not.\n            SELECT projects.id\n            FROM projects\n            WHERE projects.visibility_level IN (20, 10)\n        )\n    )\n    ORDER BY events.id DESC;\n\nThis particular query is quite the behemoth but currently this is the easiest\nway of getting a list of events for projects a user has access to.\n\nOne of the bits of information provided by an Atom feed is a timestamp\nindicating the time the feed was updated. This timestamp was generated using the\nmethod `Event.latest_update_time` which would take a collection of events and\nreturn the most recent update time. This method was implemented as the following:\n\n    def latest_update_time\n      row = select(:updated_at, :project_id).reorder(id: :desc).take\n\n      row ? row.updated_at : nil\n    end\n\nThis method is broken up in two steps:\n\n1. Order the collection in descending order, take the first record\n2. If there was a record return the `updated_at` value, otherwise return `nil`\n\nThis method was then used as the following in the Atom feed (here `xml.updated`\nwould generate an `\u003Cupdated>` XML element):\n\n    xml.updated @events.latest_update_time.xmlschema if @events.any?\n\nPerformance of this method was less than stellar (the blue bars are the timings\nof `Event.latest_update_time`):\n\n![Event.latest_update_time Timings][mr2613-timings]\n\nIn this graph we can see the timings quite often hover around 10 seconds. That's\n10 seconds _just_ to get the latest update time from the database. Ouch!\n\nAt first I started messing around with using the SQL `max()` function instead of\na combination of `ORDER BY` and `LIMIT 1`. We were using this in the past and I\nexplicitly removed it because it was performing worse at the time. Since quite a\nbit changed since then I figured it was worth re-investigating the use of this\nfunction. The process of looking into this as well as my findings can be found\nin issue [12415](https://gitlab.com/gitlab-org/gitlab-ce/issues/12415).\n\nA couple of days after I first started looking into this issue I realized there\nwas a far easier solution to this problem. Since retrieving the list of events\nitself (without using the above code) is already quite fast and is already\nsorted in the right order we can simply re-use this list. That is, we'd take the\nfollowing steps:\n\n1. Query the list of events.\n2. Cast the list of events from an ActiveRecord query result to an Array (this\n   is done anyway later on as we have to generate XML for every event).\n3. Take the `updated_at` value of the first event in this list, if present.\n\nThis led to merge request\n[\"Improve performance of retrieving last update times for events\"][mr2613]. This\nmerge request also contains a few other changes so certain records aren't loaded\ninto memory when not needed, but the gist of it is that instead of this:\n\n    xml.updated @events.latest_update_time.xmlschema if @events.any?\n\nWe now use this:\n\n    xml.updated @events[0].updated_at.xmlschema if @events[0]\n\nAs a result of this the method `Event.latest_update_time` was no longer needed\nand thus was removed. This in turn drastically reduced the loading times of all\nAtom feeds (not just user feeds).\n\n## Only set autocrlf when creating/updating files\n\nGit has an option called `core.autocrlf` which can be used to automatically\nconvert line endings in text files. This option can be set to 3 values:\n\n1. `true`: CRLF line endings are always converted to LF line endings\n2. `false`: no conversion takes place\n3. `input`: converts CRLF line endings to LF upon committing changes\n\nGitLab supports 3 ways of committing changes to a Git repository:\n\n1. Via a Git client\n2. Via the web editor\n3. Via the API\n\nIn the last 2 cases we want to make sure CRLF line endings are replaced with LF\nline endings. For example, browsers use CRLF even on non Windows platforms. To\ntake care of this our documentation recommends users to configure Git to set\n`core.autocrlf` to `input`, however we still need to take care of this ourselves\nin case a user didn't configure Git to convert line endings by default. This\nprocess took place in a method called `Repository#raw_repository` which was\nimplemented as the following:\n\n    def raw_repository\n      return nil unless path_with_namespace\n\n      @raw_repository ||= begin\n        repo = Gitlab::Git::Repository.new(path_to_repo)\n        repo.autocrlf = :input\n        repo\n      rescue Gitlab::Git::Repository::NoRepository\n        nil\n      end\n    end\n\nThis particular method is used in quite a number of places and is used on almost\nevery (if not every) project-specific page (issues, milestones, the project\nhomepage, etc). Performance of this method was, well, bad:\n\n![Gitlab::Git::Repository#autocrlf= Timings][mr2859-bars]\n\nThis particular graph plots the 95th percentile of the method\n`Gitlab::Git::Repository#autocrlf=` which is used to set the `core.autocrlf`\noption. We can see that on average the 95th percentile hovers around 500\nmilliseconds. That's 500 milliseconds on almost every page to set a Git option\nthat's already set 99% of the time. More importantly, that's 500 milliseconds of\ntime wasted on many pages where no changes are ever written to a Git repository,\nthus never using this option.\n\nIt's clear that we _don't_ want to run this on every page, especially when the\noption is not going to be used. However, we still have to make sure this option\nis set when we _do_ need it. At this point my first thought was to see the\noverhead of always writing this option versus only writing this when actually\nneeded. In Ruby code this would roughly translate to:\n\n    repo = Gitlab::Git::Repository.new(path_to_repo)\n\n    # Only set autocrlf to :input if it's not already set to :input\n    repo.autocrlf = :input unless repo.autocrlf == :input\n\nThe idea was that when sharing a disk over the network (e.g. via an NFS server)\na read is probably much faster than a write. A write may also end up locking\nfiles for the duration, possibly blocking other read operations. To test this I\nwrote a script that would perform said operation a number of times and write the\ntimings to InfluxDB. This script is as the following:\n\n    require 'rugged'\n    require 'thread'\n    require 'benchmark'\n    require 'influxdb'\n\n    Thread.abort_on_exception = true\n\n    path = '/var/opt/gitlab/git-data/repositories/yorickpeterse/cat-pictures.git'\n    key  = 'core.autocrlf'\n    read = true\n\n    influx_options = { udp: { host: 'HOST', port: PORT } }\n\n    threads = 10.times.map do\n      Thread.new do\n        client = InfluxDB::Client.new(influx_options)\n\n        while read\n          time = Benchmark.measure do\n            repo = Rugged::Repository.new(path)\n\n            repo.config[key] = 'input' unless repo.config[key] == 'input'\n          end\n\n          ms = time.real * 1000\n\n          client.write_point('rugged_config_cas', values: { duration: ms })\n\n          sleep 0.05\n        end\n      end\n    end\n\n    sleep(120)\n\n    read = false\n\n    threads.each(&:join)\n\n    Rugged::Repository.new(path).config[key] = 'input'\n\nHere HOST and PORT were replaced with the hostname and port number of our\nInfluxDB server.\n\nRunning this script produced the following graph:\n\n![Timings for writing autocrlf when needed](https://about.gitlab.com/images/making_gitlab_faster/autocrlf_write_when_needed.png)\n\nNext I modified this script to simply always write the autocrlf option, this\nproduced the following graph:\n\n![Timings for always writing autocrlf](https://about.gitlab.com/images/making_gitlab_faster/autocrlf_always_write.png)\n\nFinally I modified the script to simply load the repository as-is, this produced\nthe following graph:\n\n![Timings for only reading](https://about.gitlab.com/images/making_gitlab_faster/autocrlf_read_only.png)\n\nIn all 3 cases we can see there's not really a clear difference in timings,\nleading me to believe there's no particular benefit to only writing the option\nwhen not already set to \"input\".\n\nI spent some more time trying out different things to see how they would impact\nperformance but sadly didn't get much out of it. The details can be found in the\nvarious comments for [issue 13457](https://gitlab.com/gitlab-org/gitlab-ce/issues/13457).\n\nA day later I and [Jacob Vosmaer][jacob] decided to double check the idea of\nwriting only when needed by applying a small patch to GitLab.com. This patch\nmodified `Repository#raw_repository` to the autocrlf option would only be\nwritten when needed just like the script above. We also made sure to measure the\ntimings of both reading and writing this option. After deploying this patch and\nwaiting for about half an hour to get enough data the timings were as the\nfollowing:\n\n![autocrlf reads vs writes](https://about.gitlab.com/images/making_gitlab_faster/autocrlf_reads_vs_writes.png)\n\nThis graph shows a nice drop in timings for writing the autocrlf option, sadly\nat the cost of an increase in timings for reading the autocrlf option. In other\nwords, this change didn't actually solve anything but instead just moved the\nproblem from writing an option to just reading the option.\n\nAfter discussing this with Jacob he suggested it may be an even better idea to\nonly set this option where we actually need it to, instead of checking (and\npotentially writing) it on every page that happens to use\n`Repository#raw_repository`. After all, the best way to speed code up is to\nremove it entirely (or at least as much as possible).\n\nThis lead to merge request\n[\"Only set autocrlf when creating/updating files\"][mr2859] which does exactly\nthat. The impact of this change can be seen in the following graph:\n\n![Merge Request Timings Impact](https://about.gitlab.com/images/making_gitlab_faster/autocrlf_timings_impact.png)\n\nThis graph shows the 95th percentile, 99th percentile, and the mean per 30\nminutes. The drop around the 20th is after the above merge request was deployed\nto GitLab.com. The changes in this merge request resulted in the timings going\nfrom between 70 milliseconds and 2.1 seconds to less than 15 milliseconds.\n\n## Conclusion\n\nIn this article I only highlighted 3 merge requests that made it into 8.5.0. The\nfollowing performance related merge requests are also included in 8.5.0:\n\n* [First pass at deleting projects in the background](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2569)\n* [Background process note logic](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2631)\n* [Page project list on dashboard](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2689)\n* [Cache BroadcastMessage.current](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2633)\n* [Smarter flushing of branch statistics caches](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2769)\n* [Cache various Repository Git operations](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2752)\n* [Dedicated method for counting commits between refs](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2707)\n\nThese are just a few of the performance changes we've made over the past few\nmonths, and they certainly won't be the last as there's still a lot of work to\nbe done.\n\n[mr2625]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2625\n[mr2625-timings]: /images/making_gitlab_faster/merge_request_closes_issues.png\n[mr2613]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2613\n[mr2613-timings]: /images/making_gitlab_faster/event_latest_update_time.png\n[mr2859]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2859\n[mr2859-bars]: /images/making_gitlab_faster/gitlab_git_repository_autocrlf_bars.png\n[monitoring]: http://doc.gitlab.com/ce/monitoring/performance/introduction.html\n[influxdb]: https://influxdata.com/time-series-platform/influxdb/\n[grafana]: http://grafana.org/\n[rblineprof]: https://github.com/peek/peek-rblineprof\n[benchmark-ips]: https://github.com/evanphx/benchmark-ips\n[benchmark-ips-readme]: https://github.com/evanphx/benchmark-ips/blob/master/README.md\n[jacob]: https://gitlab.com/jacobvosmaer\n","engineering",{"slug":23,"featured":6,"template":24},"making-gitlab-faster","BlogPost","content:en-us:blog:making-gitlab-faster.yml","yaml","Making Gitlab Faster","content","en-us/blog/making-gitlab-faster.yml","en-us/blog/making-gitlab-faster","yml",{"_path":33,"_dir":34,"_draft":6,"_partial":6,"_locale":7,"data":35,"_id":445,"_type":26,"title":446,"_source":28,"_file":447,"_stem":448,"_extension":31},"/shared/en-us/main-navigation","en-us",{"logo":36,"freeTrial":41,"sales":46,"login":51,"items":56,"search":386,"minimal":417,"duo":436},{"config":37},{"href":38,"dataGaName":39,"dataGaLocation":40},"/","gitlab logo","header",{"text":42,"config":43},"Get free trial",{"href":44,"dataGaName":45,"dataGaLocation":40},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com&glm_content=default-saas-trial/","free trial",{"text":47,"config":48},"Talk to sales",{"href":49,"dataGaName":50,"dataGaLocation":40},"/sales/","sales",{"text":52,"config":53},"Sign in",{"href":54,"dataGaName":55,"dataGaLocation":40},"https://gitlab.com/users/sign_in/","sign in",[57,101,197,202,307,367],{"text":58,"config":59,"cards":61,"footer":84},"Platform",{"dataNavLevelOne":60},"platform",[62,68,76],{"title":58,"description":63,"link":64},"The most comprehensive AI-powered DevSecOps Platform",{"text":65,"config":66},"Explore our Platform",{"href":67,"dataGaName":60,"dataGaLocation":40},"/platform/",{"title":69,"description":70,"link":71},"GitLab Duo (AI)","Build software faster with AI at every stage of development",{"text":72,"config":73},"Meet GitLab Duo",{"href":74,"dataGaName":75,"dataGaLocation":40},"/gitlab-duo/","gitlab duo ai",{"title":77,"description":78,"link":79},"Why GitLab","10 reasons why Enterprises choose GitLab",{"text":80,"config":81},"Learn more",{"href":82,"dataGaName":83,"dataGaLocation":40},"/why-gitlab/","why gitlab",{"title":85,"items":86},"Get started with",[87,92,97],{"text":88,"config":89},"Platform Engineering",{"href":90,"dataGaName":91,"dataGaLocation":40},"/solutions/platform-engineering/","platform engineering",{"text":93,"config":94},"Developer Experience",{"href":95,"dataGaName":96,"dataGaLocation":40},"/developer-experience/","Developer experience",{"text":98,"config":99},"MLOps",{"href":100,"dataGaName":98,"dataGaLocation":40},"/topics/devops/the-role-of-ai-in-devops/",{"text":102,"left":103,"config":104,"link":106,"lists":110,"footer":179},"Product",true,{"dataNavLevelOne":105},"solutions",{"text":107,"config":108},"View all Solutions",{"href":109,"dataGaName":105,"dataGaLocation":40},"/solutions/",[111,136,158],{"title":112,"description":113,"link":114,"items":119},"Automation","CI/CD and automation to accelerate deployment",{"config":115},{"icon":116,"href":117,"dataGaName":118,"dataGaLocation":40},"AutomatedCodeAlt","/solutions/delivery-automation/","automated software delivery",[120,124,128,132],{"text":121,"config":122},"CI/CD",{"href":123,"dataGaLocation":40,"dataGaName":121},"/solutions/continuous-integration/",{"text":125,"config":126},"AI-Assisted Development",{"href":74,"dataGaLocation":40,"dataGaName":127},"AI assisted development",{"text":129,"config":130},"Source Code Management",{"href":131,"dataGaLocation":40,"dataGaName":129},"/solutions/source-code-management/",{"text":133,"config":134},"Automated Software Delivery",{"href":117,"dataGaLocation":40,"dataGaName":135},"Automated software delivery",{"title":137,"description":138,"link":139,"items":144},"Security","Deliver code faster without compromising security",{"config":140},{"href":141,"dataGaName":142,"dataGaLocation":40,"icon":143},"/solutions/security-compliance/","security and compliance","ShieldCheckLight",[145,148,153],{"text":146,"config":147},"Security & Compliance",{"href":141,"dataGaLocation":40,"dataGaName":146},{"text":149,"config":150},"Software Supply Chain Security",{"href":151,"dataGaLocation":40,"dataGaName":152},"/solutions/supply-chain/","Software supply chain security",{"text":154,"config":155},"Compliance & Governance",{"href":156,"dataGaLocation":40,"dataGaName":157},"/solutions/continuous-software-compliance/","Compliance and governance",{"title":159,"link":160,"items":165},"Measurement",{"config":161},{"icon":162,"href":163,"dataGaName":164,"dataGaLocation":40},"DigitalTransformation","/solutions/visibility-measurement/","visibility and measurement",[166,170,174],{"text":167,"config":168},"Visibility & Measurement",{"href":163,"dataGaLocation":40,"dataGaName":169},"Visibility and Measurement",{"text":171,"config":172},"Value Stream Management",{"href":173,"dataGaLocation":40,"dataGaName":171},"/solutions/value-stream-management/",{"text":175,"config":176},"Analytics & Insights",{"href":177,"dataGaLocation":40,"dataGaName":178},"/solutions/analytics-and-insights/","Analytics and insights",{"title":180,"items":181},"GitLab for",[182,187,192],{"text":183,"config":184},"Enterprise",{"href":185,"dataGaLocation":40,"dataGaName":186},"/enterprise/","enterprise",{"text":188,"config":189},"Small Business",{"href":190,"dataGaLocation":40,"dataGaName":191},"/small-business/","small business",{"text":193,"config":194},"Public Sector",{"href":195,"dataGaLocation":40,"dataGaName":196},"/solutions/public-sector/","public sector",{"text":198,"config":199},"Pricing",{"href":200,"dataGaName":201,"dataGaLocation":40,"dataNavLevelOne":201},"/pricing/","pricing",{"text":203,"config":204,"link":206,"lists":210,"feature":294},"Resources",{"dataNavLevelOne":205},"resources",{"text":207,"config":208},"View all resources",{"href":209,"dataGaName":205,"dataGaLocation":40},"/resources/",[211,244,266],{"title":212,"items":213},"Getting started",[214,219,224,229,234,239],{"text":215,"config":216},"Install",{"href":217,"dataGaName":218,"dataGaLocation":40},"/install/","install",{"text":220,"config":221},"Quick start guides",{"href":222,"dataGaName":223,"dataGaLocation":40},"/get-started/","quick setup checklists",{"text":225,"config":226},"Learn",{"href":227,"dataGaLocation":40,"dataGaName":228},"https://university.gitlab.com/","learn",{"text":230,"config":231},"Product documentation",{"href":232,"dataGaName":233,"dataGaLocation":40},"https://docs.gitlab.com/","product documentation",{"text":235,"config":236},"Best practice videos",{"href":237,"dataGaName":238,"dataGaLocation":40},"/getting-started-videos/","best practice videos",{"text":240,"config":241},"Integrations",{"href":242,"dataGaName":243,"dataGaLocation":40},"/integrations/","integrations",{"title":245,"items":246},"Discover",[247,252,256,261],{"text":248,"config":249},"Customer success stories",{"href":250,"dataGaName":251,"dataGaLocation":40},"/customers/","customer success stories",{"text":253,"config":254},"Blog",{"href":255,"dataGaName":5,"dataGaLocation":40},"/blog/",{"text":257,"config":258},"Remote",{"href":259,"dataGaName":260,"dataGaLocation":40},"https://handbook.gitlab.com/handbook/company/culture/all-remote/","remote",{"text":262,"config":263},"TeamOps",{"href":264,"dataGaName":265,"dataGaLocation":40},"/teamops/","teamops",{"title":267,"items":268},"Connect",[269,274,279,284,289],{"text":270,"config":271},"GitLab Services",{"href":272,"dataGaName":273,"dataGaLocation":40},"/services/","services",{"text":275,"config":276},"Community",{"href":277,"dataGaName":278,"dataGaLocation":40},"/community/","community",{"text":280,"config":281},"Forum",{"href":282,"dataGaName":283,"dataGaLocation":40},"https://forum.gitlab.com/","forum",{"text":285,"config":286},"Events",{"href":287,"dataGaName":288,"dataGaLocation":40},"/events/","events",{"text":290,"config":291},"Partners",{"href":292,"dataGaName":293,"dataGaLocation":40},"/partners/","partners",{"backgroundColor":295,"textColor":296,"text":297,"image":298,"link":302},"#2f2a6b","#fff","Insights for the future of software development",{"altText":299,"config":300},"the source promo card",{"src":301},"/images/navigation/the-source-promo-card.svg",{"text":303,"config":304},"Read the latest",{"href":305,"dataGaName":306,"dataGaLocation":40},"/the-source/","the source",{"text":308,"config":309,"lists":311},"Company",{"dataNavLevelOne":310},"company",[312],{"items":313},[314,319,325,327,332,337,342,347,352,357,362],{"text":315,"config":316},"About",{"href":317,"dataGaName":318,"dataGaLocation":40},"/company/","about",{"text":320,"config":321,"footerGa":324},"Jobs",{"href":322,"dataGaName":323,"dataGaLocation":40},"/jobs/","jobs",{"dataGaName":323},{"text":285,"config":326},{"href":287,"dataGaName":288,"dataGaLocation":40},{"text":328,"config":329},"Leadership",{"href":330,"dataGaName":331,"dataGaLocation":40},"/company/team/e-group/","leadership",{"text":333,"config":334},"Team",{"href":335,"dataGaName":336,"dataGaLocation":40},"/company/team/","team",{"text":338,"config":339},"Handbook",{"href":340,"dataGaName":341,"dataGaLocation":40},"https://handbook.gitlab.com/","handbook",{"text":343,"config":344},"Investor relations",{"href":345,"dataGaName":346,"dataGaLocation":40},"https://ir.gitlab.com/","investor relations",{"text":348,"config":349},"Trust Center",{"href":350,"dataGaName":351,"dataGaLocation":40},"/security/","trust center",{"text":353,"config":354},"AI Transparency Center",{"href":355,"dataGaName":356,"dataGaLocation":40},"/ai-transparency-center/","ai transparency center",{"text":358,"config":359},"Newsletter",{"href":360,"dataGaName":361,"dataGaLocation":40},"/company/contact/","newsletter",{"text":363,"config":364},"Press",{"href":365,"dataGaName":366,"dataGaLocation":40},"/press/","press",{"text":368,"config":369,"lists":370},"Contact us",{"dataNavLevelOne":310},[371],{"items":372},[373,376,381],{"text":47,"config":374},{"href":49,"dataGaName":375,"dataGaLocation":40},"talk to sales",{"text":377,"config":378},"Get help",{"href":379,"dataGaName":380,"dataGaLocation":40},"/support/","get help",{"text":382,"config":383},"Customer portal",{"href":384,"dataGaName":385,"dataGaLocation":40},"https://customers.gitlab.com/customers/sign_in/","customer portal",{"close":387,"login":388,"suggestions":395},"Close",{"text":389,"link":390},"To search repositories and projects, login to",{"text":391,"config":392},"gitlab.com",{"href":54,"dataGaName":393,"dataGaLocation":394},"search login","search",{"text":396,"default":397},"Suggestions",[398,400,404,406,410,414],{"text":69,"config":399},{"href":74,"dataGaName":69,"dataGaLocation":394},{"text":401,"config":402},"Code Suggestions (AI)",{"href":403,"dataGaName":401,"dataGaLocation":394},"/solutions/code-suggestions/",{"text":121,"config":405},{"href":123,"dataGaName":121,"dataGaLocation":394},{"text":407,"config":408},"GitLab on AWS",{"href":409,"dataGaName":407,"dataGaLocation":394},"/partners/technology-partners/aws/",{"text":411,"config":412},"GitLab on Google Cloud",{"href":413,"dataGaName":411,"dataGaLocation":394},"/partners/technology-partners/google-cloud-platform/",{"text":415,"config":416},"Why GitLab?",{"href":82,"dataGaName":415,"dataGaLocation":394},{"freeTrial":418,"mobileIcon":423,"desktopIcon":428,"secondaryButton":431},{"text":419,"config":420},"Start free trial",{"href":421,"dataGaName":45,"dataGaLocation":422},"https://gitlab.com/-/trials/new/","nav",{"altText":424,"config":425},"Gitlab Icon",{"src":426,"dataGaName":427,"dataGaLocation":422},"/images/brand/gitlab-logo-tanuki.svg","gitlab icon",{"altText":424,"config":429},{"src":430,"dataGaName":427,"dataGaLocation":422},"/images/brand/gitlab-logo-type.svg",{"text":432,"config":433},"Get Started",{"href":434,"dataGaName":435,"dataGaLocation":422},"https://gitlab.com/-/trial_registrations/new?glm_source=about.gitlab.com/compare/gitlab-vs-github/","get started",{"freeTrial":437,"mobileIcon":441,"desktopIcon":443},{"text":438,"config":439},"Learn more about GitLab Duo",{"href":74,"dataGaName":440,"dataGaLocation":422},"gitlab duo",{"altText":424,"config":442},{"src":426,"dataGaName":427,"dataGaLocation":422},{"altText":424,"config":444},{"src":430,"dataGaName":427,"dataGaLocation":422},"content:shared:en-us:main-navigation.yml","Main Navigation","shared/en-us/main-navigation.yml","shared/en-us/main-navigation",{"_path":450,"_dir":34,"_draft":6,"_partial":6,"_locale":7,"title":451,"button":452,"config":456,"_id":458,"_type":26,"_source":28,"_file":459,"_stem":460,"_extension":31},"/shared/en-us/banner","GitLab Duo Agent Platform is now in public beta!",{"text":80,"config":453},{"href":454,"dataGaName":455,"dataGaLocation":40},"/gitlab-duo/agent-platform/","duo banner",{"layout":457},"release","content:shared:en-us:banner.yml","shared/en-us/banner.yml","shared/en-us/banner",{"_path":462,"_dir":34,"_draft":6,"_partial":6,"_locale":7,"data":463,"_id":668,"_type":26,"title":669,"_source":28,"_file":670,"_stem":671,"_extension":31},"/shared/en-us/main-footer",{"text":464,"source":465,"edit":471,"contribute":476,"config":481,"items":486,"minimal":660},"Git is a trademark of Software Freedom Conservancy and our use of 'GitLab' is under license",{"text":466,"config":467},"View page source",{"href":468,"dataGaName":469,"dataGaLocation":470},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/","page source","footer",{"text":472,"config":473},"Edit this page",{"href":474,"dataGaName":475,"dataGaLocation":470},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/content/","web ide",{"text":477,"config":478},"Please contribute",{"href":479,"dataGaName":480,"dataGaLocation":470},"https://gitlab.com/gitlab-com/marketing/digital-experience/about-gitlab-com/-/blob/main/CONTRIBUTING.md/","please contribute",{"twitter":482,"facebook":483,"youtube":484,"linkedin":485},"https://twitter.com/gitlab","https://www.facebook.com/gitlab","https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg","https://www.linkedin.com/company/gitlab-com",[487,510,567,596,630],{"title":58,"links":488,"subMenu":493},[489],{"text":490,"config":491},"DevSecOps platform",{"href":67,"dataGaName":492,"dataGaLocation":470},"devsecops platform",[494],{"title":198,"links":495},[496,500,505],{"text":497,"config":498},"View plans",{"href":200,"dataGaName":499,"dataGaLocation":470},"view plans",{"text":501,"config":502},"Why Premium?",{"href":503,"dataGaName":504,"dataGaLocation":470},"/pricing/premium/","why premium",{"text":506,"config":507},"Why Ultimate?",{"href":508,"dataGaName":509,"dataGaLocation":470},"/pricing/ultimate/","why ultimate",{"title":511,"links":512},"Solutions",[513,518,521,523,528,533,537,540,544,549,551,554,557,562],{"text":514,"config":515},"Digital transformation",{"href":516,"dataGaName":517,"dataGaLocation":470},"/topics/digital-transformation/","digital transformation",{"text":146,"config":519},{"href":141,"dataGaName":520,"dataGaLocation":470},"security & compliance",{"text":135,"config":522},{"href":117,"dataGaName":118,"dataGaLocation":470},{"text":524,"config":525},"Agile development",{"href":526,"dataGaName":527,"dataGaLocation":470},"/solutions/agile-delivery/","agile delivery",{"text":529,"config":530},"Cloud transformation",{"href":531,"dataGaName":532,"dataGaLocation":470},"/topics/cloud-native/","cloud transformation",{"text":534,"config":535},"SCM",{"href":131,"dataGaName":536,"dataGaLocation":470},"source code management",{"text":121,"config":538},{"href":123,"dataGaName":539,"dataGaLocation":470},"continuous integration & delivery",{"text":541,"config":542},"Value stream management",{"href":173,"dataGaName":543,"dataGaLocation":470},"value stream management",{"text":545,"config":546},"GitOps",{"href":547,"dataGaName":548,"dataGaLocation":470},"/solutions/gitops/","gitops",{"text":183,"config":550},{"href":185,"dataGaName":186,"dataGaLocation":470},{"text":552,"config":553},"Small business",{"href":190,"dataGaName":191,"dataGaLocation":470},{"text":555,"config":556},"Public sector",{"href":195,"dataGaName":196,"dataGaLocation":470},{"text":558,"config":559},"Education",{"href":560,"dataGaName":561,"dataGaLocation":470},"/solutions/education/","education",{"text":563,"config":564},"Financial services",{"href":565,"dataGaName":566,"dataGaLocation":470},"/solutions/finance/","financial services",{"title":203,"links":568},[569,571,573,575,578,580,582,584,586,588,590,592,594],{"text":215,"config":570},{"href":217,"dataGaName":218,"dataGaLocation":470},{"text":220,"config":572},{"href":222,"dataGaName":223,"dataGaLocation":470},{"text":225,"config":574},{"href":227,"dataGaName":228,"dataGaLocation":470},{"text":230,"config":576},{"href":232,"dataGaName":577,"dataGaLocation":470},"docs",{"text":253,"config":579},{"href":255,"dataGaName":5,"dataGaLocation":470},{"text":248,"config":581},{"href":250,"dataGaName":251,"dataGaLocation":470},{"text":257,"config":583},{"href":259,"dataGaName":260,"dataGaLocation":470},{"text":270,"config":585},{"href":272,"dataGaName":273,"dataGaLocation":470},{"text":262,"config":587},{"href":264,"dataGaName":265,"dataGaLocation":470},{"text":275,"config":589},{"href":277,"dataGaName":278,"dataGaLocation":470},{"text":280,"config":591},{"href":282,"dataGaName":283,"dataGaLocation":470},{"text":285,"config":593},{"href":287,"dataGaName":288,"dataGaLocation":470},{"text":290,"config":595},{"href":292,"dataGaName":293,"dataGaLocation":470},{"title":308,"links":597},[598,600,602,604,606,608,610,614,619,621,623,625],{"text":315,"config":599},{"href":317,"dataGaName":310,"dataGaLocation":470},{"text":320,"config":601},{"href":322,"dataGaName":323,"dataGaLocation":470},{"text":328,"config":603},{"href":330,"dataGaName":331,"dataGaLocation":470},{"text":333,"config":605},{"href":335,"dataGaName":336,"dataGaLocation":470},{"text":338,"config":607},{"href":340,"dataGaName":341,"dataGaLocation":470},{"text":343,"config":609},{"href":345,"dataGaName":346,"dataGaLocation":470},{"text":611,"config":612},"Sustainability",{"href":613,"dataGaName":611,"dataGaLocation":470},"/sustainability/",{"text":615,"config":616},"Diversity, inclusion and belonging (DIB)",{"href":617,"dataGaName":618,"dataGaLocation":470},"/diversity-inclusion-belonging/","Diversity, inclusion and belonging",{"text":348,"config":620},{"href":350,"dataGaName":351,"dataGaLocation":470},{"text":358,"config":622},{"href":360,"dataGaName":361,"dataGaLocation":470},{"text":363,"config":624},{"href":365,"dataGaName":366,"dataGaLocation":470},{"text":626,"config":627},"Modern Slavery Transparency Statement",{"href":628,"dataGaName":629,"dataGaLocation":470},"https://handbook.gitlab.com/handbook/legal/modern-slavery-act-transparency-statement/","modern slavery transparency statement",{"title":631,"links":632},"Contact Us",[633,636,638,640,645,650,655],{"text":634,"config":635},"Contact an expert",{"href":49,"dataGaName":50,"dataGaLocation":470},{"text":377,"config":637},{"href":379,"dataGaName":380,"dataGaLocation":470},{"text":382,"config":639},{"href":384,"dataGaName":385,"dataGaLocation":470},{"text":641,"config":642},"Status",{"href":643,"dataGaName":644,"dataGaLocation":470},"https://status.gitlab.com/","status",{"text":646,"config":647},"Terms of use",{"href":648,"dataGaName":649,"dataGaLocation":470},"/terms/","terms of use",{"text":651,"config":652},"Privacy statement",{"href":653,"dataGaName":654,"dataGaLocation":470},"/privacy/","privacy statement",{"text":656,"config":657},"Cookie preferences",{"dataGaName":658,"dataGaLocation":470,"id":659,"isOneTrustButton":103},"cookie preferences","ot-sdk-btn",{"items":661},[662,664,666],{"text":646,"config":663},{"href":648,"dataGaName":649,"dataGaLocation":470},{"text":651,"config":665},{"href":653,"dataGaName":654,"dataGaLocation":470},{"text":656,"config":667},{"dataGaName":658,"dataGaLocation":470,"id":659,"isOneTrustButton":103},"content:shared:en-us:main-footer.yml","Main Footer","shared/en-us/main-footer.yml","shared/en-us/main-footer",[673],{"_path":674,"_dir":675,"_draft":6,"_partial":6,"_locale":7,"content":676,"config":680,"_id":682,"_type":26,"title":18,"_source":28,"_file":683,"_stem":684,"_extension":31},"/en-us/blog/authors/yorick-peterse","authors",{"name":18,"config":677},{"headshot":678,"ctfId":679},"https://res.cloudinary.com/about-gitlab-com/image/upload/v1749659488/Blog/Author%20Headshots/gitlab-logo-extra-whitespace.png","Yorick-Peterse",{"template":681},"BlogAuthor","content:en-us:blog:authors:yorick-peterse.yml","en-us/blog/authors/yorick-peterse.yml","en-us/blog/authors/yorick-peterse",{"_path":686,"_dir":34,"_draft":6,"_partial":6,"_locale":7,"header":687,"eyebrow":688,"blurb":689,"button":690,"secondaryButton":694,"_id":696,"_type":26,"title":697,"_source":28,"_file":698,"_stem":699,"_extension":31},"/shared/en-us/next-steps","Start shipping better software faster","50%+ of the Fortune 100 trust GitLab","See what your team can do with the intelligent\n\n\nDevSecOps platform.\n",{"text":42,"config":691},{"href":692,"dataGaName":45,"dataGaLocation":693},"https://gitlab.com/-/trial_registrations/new?glm_content=default-saas-trial&glm_source=about.gitlab.com/","feature",{"text":47,"config":695},{"href":49,"dataGaName":50,"dataGaLocation":693},"content:shared:en-us:next-steps.yml","Next Steps","shared/en-us/next-steps.yml","shared/en-us/next-steps",1753475335337]